Skip to content

Commit b55a08b

Browse files
authored
feat: check cycle reference (#22)
Fixes: #20
1 parent a18516c commit b55a08b

File tree

7 files changed

+128
-146
lines changed

7 files changed

+128
-146
lines changed

examples/basic/pages/index.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,22 @@ function aPlusB (a: number, b: number) {
1111
return a + b
1212
}
1313

14+
const loopObject = {
15+
foo: 1,
16+
goo: 'string'
17+
} as Record<string, any>
18+
19+
loopObject.self = loopObject
20+
21+
const loopArray = [
22+
loopObject
23+
]
24+
25+
loopArray[1] = loopArray
26+
1427
const example = {
28+
loopObject,
29+
loopArray,
1530
string: 'this is a string',
1631
integer: 42,
1732
array: [19, 19, 810, 'test', NaN],

src/components/DataKeyPair.tsx

+15-7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useTextColor } from '../hooks/useColor'
1313
import { useJsonViewerStore } from '../stores/JsonViewerStore'
1414
import { useTypeComponents } from '../stores/typeRegistry'
1515
import type { DataItemProps } from '../type'
16+
import { isCycleReference } from '../utils'
1617
import { DataBox } from './mui/DataBox'
1718

1819
export type DataKeyPairProps = {
@@ -35,8 +36,14 @@ export const DataKeyPair: React.FC<DataKeyPairProps> = (props) => {
3536
return hoverPath && path.every((value, index) => value === hoverPath[index])
3637
}, [hoverPath, path])
3738
const setHover = useJsonViewerStore(store => store.setHover)
39+
const root = useJsonViewerStore(store => store.value)
40+
const isTrap = useMemo(() => isCycleReference(root, path, value), [path, root, value])
41+
const defaultCollapsed = useJsonViewerStore(store => store.defaultCollapsed)
42+
// do not inspect when it is a cycle reference, otherwise there will have a loop
3843
const [inspect, setInspect] = useState(
39-
!useJsonViewerStore(store => store.defaultCollapsed)
44+
isTrap
45+
? false
46+
: !defaultCollapsed
4047
)
4148
const [editing, setEditing] = useState(false)
4249
const onChange = useJsonViewerStore(store => store.onChange)
@@ -46,7 +53,7 @@ export const DataKeyPair: React.FC<DataKeyPairProps> = (props) => {
4653
const { Component, PreComponent, PostComponent, Editor } = useTypeComponents(
4754
value)
4855
const rootName = useJsonViewerStore(store => store.rootName)
49-
const isRoot = useJsonViewerStore(store => store.value) === value
56+
const isRoot = root === value
5057
const isNumberKey = Number.isInteger(Number(key))
5158
const displayKey = isRoot ? rootName : key
5259
const downstreamProps: DataItemProps = useMemo(() => ({
@@ -169,11 +176,12 @@ export const DataKeyPair: React.FC<DataKeyPairProps> = (props) => {
169176
{PreComponent && <PreComponent {...downstreamProps}/>}
170177
{(isHover && expandable && inspect) && actionIcons}
171178
</DataBox>
172-
{editing
173-
? (Editor && <Editor value={tempValue} setValue={setTempValue}/>)
174-
: Component
175-
? <Component {...downstreamProps}/>
176-
: <Box component='span'
179+
{
180+
editing
181+
? (Editor && <Editor value={tempValue} setValue={setTempValue}/>)
182+
: (Component)
183+
? <Component {...downstreamProps}/>
184+
: <Box component='span'
177185
className='data-value-fallback'>{JSON.stringify(value)}</Box>
178186
}
179187
{PostComponent && <PostComponent {...downstreamProps}/>}

src/components/DataTypes/Array.tsx

-112
This file was deleted.

src/components/DataTypes/Object.tsx

+60-13
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import React, { useMemo } from 'react'
44
import { useTextColor } from '../../hooks/useColor'
55
import { useJsonViewerStore } from '../../stores/JsonViewerStore'
66
import type { DataItemProps } from '../../type'
7+
import { isCycleReference } from '../../utils'
78
import { DataKeyPair } from '../DataKeyPair'
9+
import { CircularArrowsIcon } from '../icons/CircularArrowsIcon'
810

911
const objectLb = '{'
1012
const arrayLb = '['
@@ -13,11 +15,16 @@ const arrayRb = ']'
1315

1416
export const PreObjectType: React.FC<DataItemProps<object>> = (props) => {
1517
const metadataColor = useJsonViewerStore(store => store.colorNamespace.base04)
18+
const textColor = useTextColor()
1619
const isArray = useMemo(() => Array.isArray(props.value), [props.value])
1720
const sizeOfValue = useMemo(
1821
() => props.inspect ? `${Object.keys(props.value).length} Items` : '',
1922
[props.inspect, props.value]
2023
)
24+
const rootValue = useJsonViewerStore(store => store.value)
25+
const isTrap = useMemo(
26+
() => isCycleReference(rootValue, props.path, props.value),
27+
[props.path, props.value, rootValue])
2128
return (
2229
<Box
2330
component='span' className='data-object-start'
@@ -36,6 +43,10 @@ export const PreObjectType: React.FC<DataItemProps<object>> = (props) => {
3643
>
3744
{sizeOfValue}
3845
</Box>
46+
{isTrap
47+
? <CircularArrowsIcon
48+
sx={{ fontSize: 12, color: textColor, pl: sizeOfValue ? 0.5 : 0 }}/>
49+
: null}
3950
</Box>
4051
)
4152
}
@@ -66,14 +77,48 @@ export const PostObjectType: React.FC<DataItemProps<object>> = (props) => {
6677

6778
export const ObjectType: React.FC<DataItemProps<object>> = (props) => {
6879
const keyColor = useTextColor()
69-
const elements = useMemo(() => (
70-
Object.entries(props.value).map(([key, value]) => {
71-
const path = [...props.path, key]
72-
return (
73-
<DataKeyPair key={key} path={path} value={value}/>
74-
)
75-
})
76-
), [props.path, props.value])
80+
const groupArraysAfterLength = useJsonViewerStore(
81+
store => store.groupArraysAfterLength)
82+
const rootValue = useJsonViewerStore(store => store.value)
83+
const isTrap = useMemo(
84+
() => isCycleReference(rootValue, props.path, props.value),
85+
[props.path, props.value, rootValue]
86+
)
87+
const elements = useMemo(() => {
88+
if (Array.isArray(props.value)) {
89+
if (props.value.length <= groupArraysAfterLength) {
90+
return props.value.map((value, index) => {
91+
const path = [...props.path, index]
92+
return (
93+
<DataKeyPair key={index} path={path} value={value}/>
94+
)
95+
})
96+
}
97+
const value = props.value.reduce<unknown[][]>((array, value, index) => {
98+
const target = Math.floor(index / groupArraysAfterLength)
99+
if (array[target]) {
100+
array[target].push(value)
101+
} else {
102+
array[target] = [value]
103+
}
104+
return array
105+
}, [])
106+
107+
return value.map((list, index) => {
108+
const path = [...props.path]
109+
return (
110+
<DataKeyPair key={index} path={path} value={list} nested/>
111+
)
112+
})
113+
} else {
114+
return Object.entries(props.value).map(([key, value]) => {
115+
const path = [...props.path, key]
116+
return (
117+
<DataKeyPair key={key} path={path} value={value}/>
118+
)
119+
})
120+
}
121+
}, [props.value, props.path, groupArraysAfterLength])
77122
return (
78123
<Box
79124
className='data-object'
@@ -86,11 +131,13 @@ export const ObjectType: React.FC<DataItemProps<object>> = (props) => {
86131
{
87132
props.inspect
88133
? elements
89-
: (
90-
<Box component='span' className='data-object-body'>
91-
...
92-
</Box>
93-
)
134+
: !isTrap
135+
? (
136+
<Box component='span' className='data-object-body'>
137+
...
138+
</Box>
139+
)
140+
: null
94141
}
95142
</Box>
96143
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { SvgIcon, SvgIconProps } from '@mui/material'
2+
import type React from 'react'
3+
4+
export const CircularArrowsIcon: React.FC<SvgIconProps> = (props) => {
5+
return (
6+
<SvgIcon {...props}>
7+
<path d='M 12 2 C 10.615 1.998 9.214625 2.2867656 7.890625 2.8847656 L 8.9003906 4.6328125 C 9.9043906 4.2098125 10.957 3.998 12 4 C 15.080783 4 17.738521 5.7633175 19.074219 8.3222656 L 17.125 9 L 21.25 11 L 22.875 7 L 20.998047 7.6523438 C 19.377701 4.3110398 15.95585 2 12 2 z M 6.5097656 4.4882812 L 2.2324219 5.0820312 L 3.734375 6.3808594 C 1.6515335 9.4550558 1.3615962 13.574578 3.3398438 17 C 4.0308437 18.201 4.9801562 19.268234 6.1601562 20.115234 L 7.1699219 18.367188 C 6.3019219 17.710187 5.5922656 16.904 5.0722656 16 C 3.5320014 13.332354 3.729203 10.148679 5.2773438 7.7128906 L 6.8398438 9.0625 L 6.5097656 4.4882812 z M 19.929688 13 C 19.794687 14.08 19.450734 15.098 18.927734 16 C 17.386985 18.668487 14.531361 20.090637 11.646484 19.966797 L 12.035156 17.9375 L 8.2402344 20.511719 L 10.892578 23.917969 L 11.265625 21.966797 C 14.968963 22.233766 18.681899 20.426323 20.660156 17 C 21.355156 15.801 21.805219 14.445 21.949219 13 L 19.929688 13 z'/>
8+
</SvgIcon>
9+
)
10+
}

src/stores/typeRegistry.tsx

-14
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@ import { Box } from '@mui/material'
22
import { DevelopmentError } from '@textea/dev-kit/utils'
33
import React, { useMemo } from 'react'
44

5-
import {
6-
ArrayType,
7-
PostArrayType,
8-
PreArrayType
9-
} from '../components/DataTypes/Array'
105
import { createEasyType } from '../components/DataTypes/createEasyType'
116
import {
127
FunctionType, PostFunctionType,
@@ -205,15 +200,6 @@ registerType<number>(
205200
}
206201
)
207202

208-
registerType<unknown[]>(
209-
{
210-
is: (value): value is unknown[] => Array.isArray(value),
211-
Component: ArrayType,
212-
PreComponent: PreArrayType,
213-
PostComponent: PostArrayType
214-
}
215-
)
216-
217203
// fallback for all data like 'object'
218204
registerType<object>(
219205
{

src/utils/index.ts

+28
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,31 @@ export const applyValue = (obj: any, path: (string | number)[], value: any) => {
1515
}
1616
return obj
1717
}
18+
19+
export const isCycleReference = (root: any, path: (string | number)[], value: unknown): boolean => {
20+
if (root === null || value === null) {
21+
return false
22+
}
23+
if (typeof root !== 'object') {
24+
return false
25+
}
26+
if (typeof value !== 'object') {
27+
return false
28+
}
29+
if (Object.is(root, value) && path.length !== 0) {
30+
return true
31+
}
32+
const arr = [...path]
33+
let currentRoot = root
34+
while (currentRoot !== value || arr.length !== 0) {
35+
if (typeof currentRoot !== 'object' || currentRoot === null) {
36+
return false
37+
}
38+
const target = arr.shift()!
39+
if (Object.is(currentRoot, value)) {
40+
return true
41+
}
42+
currentRoot = currentRoot[target]
43+
}
44+
return false
45+
}

0 commit comments

Comments
 (0)