diff --git a/editor/src/components/inspector/sections/component-section/cartouche-control.tsx b/editor/src/components/inspector/sections/component-section/cartouche-control.tsx
index 3525a5e3524a..498b041ea685 100644
--- a/editor/src/components/inspector/sections/component-section/cartouche-control.tsx
+++ b/editor/src/components/inspector/sections/component-section/cartouche-control.tsx
@@ -6,7 +6,7 @@ import { DataCartoucheInner } from './data-reference-cartouche'
import { NO_OP } from '../../../../core/shared/utils'
import type { ElementPath, PropertyPath } from '../../../../core/shared/project-file-types'
import * as EPP from '../../../template-property-path'
-import { traceDataFromProp } from '../../../../core/data-tracing/data-tracing'
+import { dataPathSuccess, traceDataFromProp } from '../../../../core/data-tracing/data-tracing'
import { Substores, useEditorState } from '../../../editor/store/store-hook'
interface IdentifierExpressionCartoucheControlProps {
@@ -31,7 +31,7 @@ export const IdentifierExpressionCartoucheControl = React.memo(
EPP.create(props.elementPath, props.propertyPath),
store.editor.jsxMetadata,
store.editor.projectContents,
- [],
+ dataPathSuccess([]),
).type === 'hook-result',
'IdentifierExpressionCartoucheControl trace',
)
diff --git a/editor/src/components/inspector/sections/component-section/data-reference-cartouche.tsx b/editor/src/components/inspector/sections/component-section/data-reference-cartouche.tsx
index 92adeb2cc702..fc62097921eb 100644
--- a/editor/src/components/inspector/sections/component-section/data-reference-cartouche.tsx
+++ b/editor/src/components/inspector/sections/component-section/data-reference-cartouche.tsx
@@ -16,7 +16,7 @@ import { useDataPickerButton } from './component-section'
import { useDispatch } from '../../../editor/store/dispatch-context'
import { selectComponents } from '../../../editor/actions/meta-actions'
import { when } from '../../../../utils/react-conditionals'
-import { traceDataFromElement } from '../../../../core/data-tracing/data-tracing'
+import { dataPathSuccess, traceDataFromElement } from '../../../../core/data-tracing/data-tracing'
import type { DataPickerType } from './data-picker-popup'
import type { RenderedAt } from '../../../editor/store/editor-state'
@@ -52,7 +52,7 @@ export const DataReferenceCartoucheControl = React.memo(
props.surroundingScope,
store.editor.jsxMetadata,
store.editor.projectContents,
- [],
+ dataPathSuccess([]),
)
},
'IdentifierExpressionCartoucheControl trace',
diff --git a/editor/src/core/data-tracing/data-tracing.spec.ts b/editor/src/core/data-tracing/data-tracing.spec.ts
index da7d10d6f9e4..b04e9c945133 100644
--- a/editor/src/core/data-tracing/data-tracing.spec.ts
+++ b/editor/src/core/data-tracing/data-tracing.spec.ts
@@ -14,6 +14,8 @@ import type { JSExpression } from '../shared/element-template'
import type { ElementPath } from '../shared/project-file-types'
import * as PP from '../shared/property-path'
import {
+ dataPathNotPossible,
+ dataPathSuccess,
dataTracingFailed,
dataTracingToAHookCall,
dataTracingToElementAtScope,
@@ -40,11 +42,15 @@ describe('Data Tracing', () => {
EPP.create(EP.fromString('sb/app:my-component'), PP.create('title')),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
- dataTracingToLiteralAttribute(EP.fromString('sb/app:my-component'), PP.create('title'), []),
+ dataTracingToLiteralAttribute(
+ EP.fromString('sb/app:my-component'),
+ PP.create('title'),
+ dataPathSuccess([]),
+ ),
)
})
@@ -68,14 +74,14 @@ describe('Data Tracing', () => {
EPP.create(EP.fromString('sb/app:my-component/inner-child'), PP.create('title')),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
dataTracingToLiteralAttribute(
EP.fromString('sb/app:my-component/inner-child'),
PP.create('title'),
- [],
+ dataPathSuccess([]),
),
)
})
@@ -100,11 +106,15 @@ describe('Data Tracing', () => {
EPP.create(EP.fromString('sb/app:my-component:component-root'), PP.create('title')),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
- dataTracingToLiteralAttribute(EP.fromString('sb/app:my-component'), PP.create('title'), []),
+ dataTracingToLiteralAttribute(
+ EP.fromString('sb/app:my-component'),
+ PP.create('title'),
+ dataPathSuccess([]),
+ ),
)
})
@@ -135,15 +145,19 @@ describe('Data Tracing', () => {
),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
- dataTracingToLiteralAttribute(EP.fromString('sb/app:my-component'), PP.create('title'), []),
+ dataTracingToLiteralAttribute(
+ EP.fromString('sb/app:my-component'),
+ PP.create('title'),
+ dataPathSuccess([]),
+ ),
)
})
- it('Traces back a destrucuted prop to a string literal jsx attribute', async () => {
+ it('Traces back a destructed prop to a string literal jsx attribute', async () => {
const editor = await renderTestEditorWithCode(
makeTestProjectCodeWithStoryboard(`
function MyComponent({ propA, title, propC, ...restOfProps }) {
@@ -163,11 +177,15 @@ describe('Data Tracing', () => {
EPP.create(EP.fromString('sb/app:my-component:component-root'), PP.create('title')),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
- dataTracingToLiteralAttribute(EP.fromString('sb/app:my-component'), PP.create('title'), []),
+ dataTracingToLiteralAttribute(
+ EP.fromString('sb/app:my-component'),
+ PP.create('title'),
+ dataPathSuccess([]),
+ ),
)
})
@@ -191,11 +209,15 @@ describe('Data Tracing', () => {
EPP.create(EP.fromString('sb/app:my-component:component-root'), PP.create('title')),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
- dataTracingToLiteralAttribute(EP.fromString('sb/app:my-component'), PP.create('title'), []),
+ dataTracingToLiteralAttribute(
+ EP.fromString('sb/app:my-component'),
+ PP.create('title'),
+ dataPathSuccess([]),
+ ),
)
})
@@ -226,11 +248,15 @@ describe('Data Tracing', () => {
),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
- dataTracingToLiteralAttribute(EP.fromString('sb/app:my-component'), PP.create('title'), []),
+ dataTracingToLiteralAttribute(
+ EP.fromString('sb/app:my-component'),
+ PP.create('title'),
+ dataPathSuccess([]),
+ ),
)
})
@@ -254,13 +280,15 @@ describe('Data Tracing', () => {
EPP.create(EP.fromString('sb/app:my-component:component-root'), PP.create('title')),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
- dataTracingToLiteralAttribute(EP.fromString('sb/app:my-component'), PP.create('doc'), [
- 'title',
- ]),
+ dataTracingToLiteralAttribute(
+ EP.fromString('sb/app:my-component'),
+ PP.create('doc'),
+ dataPathSuccess(['title']),
+ ),
)
})
@@ -284,15 +312,15 @@ describe('Data Tracing', () => {
EPP.create(EP.fromString('sb/app:my-component:component-root'), PP.create('title')),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
- dataTracingToLiteralAttribute(EP.fromString('sb/app:my-component'), PP.create('doc'), [
- 'very',
- 'deep',
- 'title',
- ]),
+ dataTracingToLiteralAttribute(
+ EP.fromString('sb/app:my-component'),
+ PP.create('doc'),
+ dataPathSuccess(['very', 'deep', 'title']),
+ ),
)
})
@@ -323,16 +351,15 @@ describe('Data Tracing', () => {
),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
- dataTracingToLiteralAttribute(EP.fromString('sb/app:my-component'), PP.create('doc'), [
- 'very',
- 'deep',
- 'title',
- 'value',
- ]),
+ dataTracingToLiteralAttribute(
+ EP.fromString('sb/app:my-component'),
+ PP.create('doc'),
+ dataPathSuccess(['very', 'deep', 'title', 'value']),
+ ),
)
})
})
@@ -366,14 +393,14 @@ describe('Data Tracing', () => {
EPP.create(EP.fromString('sb/app:my-component:component-root'), PP.create('title')),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
dataTracingToAHookCall(
EP.fromString('sb/app:my-component:component-root'),
'useLoaderData',
- ['reviews', '0', 'hello'],
+ dataPathSuccess(['reviews', '0', 'hello']),
),
)
})
@@ -405,14 +432,14 @@ describe('Data Tracing', () => {
EPP.create(EP.fromString('sb/app:my-component:component-root'), PP.create('title')),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
dataTracingToAHookCall(
EP.fromString('sb/app:my-component:component-root'),
'useLoaderData',
- ['reviews', '0', 'hello'],
+ dataPathSuccess(['reviews', '0', 'hello']),
),
)
})
@@ -449,14 +476,14 @@ describe('Data Tracing', () => {
),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
dataTracingToAHookCall(
EP.fromString('sb/app:my-component:component-root'),
'useLoaderData',
- ['title'],
+ dataPathSuccess(['title']),
),
)
})
@@ -486,19 +513,56 @@ describe('Data Tracing', () => {
EPP.create(EP.fromString('sb/app:my-component:component-root'), PP.create('title')),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
dataTracingToAHookCall(
EP.fromString('sb/app:my-component:component-root'),
'useLoaderData',
- ['title'],
+ dataPathSuccess(['title']),
),
)
})
- it('Does not trace back a prop through an array destructured hook', async () => {
+ it('traces back through a spread value in an object destructured hook value', async () => {
+ const editor = await renderTestEditorWithCode(
+ makeTestProjectCodeWithStoryboard(`
+ function useObject() {
+ return { first: 'first', second: 'second', third: 'third', fourth: 'fourth' }
+ }
+
+ function MyComponent(props) {
+ const { first, second, ...rest } = useObject()
+ return
+ }
+
+ function App() {
+ return
+ }
+ `),
+ 'await-first-dom-report',
+ )
+
+ await focusOnComponentForTest(editor, EP.fromString('sb/app:my-component'))
+
+ const traceResult = traceDataFromProp(
+ EPP.create(EP.fromString('sb/app:my-component:component-root'), PP.create('title')),
+ editor.getEditorState().editor.jsxMetadata,
+ editor.getEditorState().editor.projectContents,
+ dataPathSuccess([]),
+ )
+
+ expect(traceResult).toEqual(
+ dataTracingToAHookCall(
+ EP.fromString('sb/app:my-component:component-root'),
+ 'useObject',
+ dataPathSuccess(['third']),
+ ),
+ )
+ })
+
+ it('traces back through an array destructured hook, but without a path', async () => {
const editor = await renderTestEditorWithCode(
makeTestProjectCodeWithStoryboard(`
function useArray() {
@@ -523,10 +587,16 @@ describe('Data Tracing', () => {
EPP.create(EP.fromString('sb/app:my-component:component-root'), PP.create('title')),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
- expect(traceResult).toEqual(dataTracingFailed('Could not find a hook call'))
+ expect(traceResult).toEqual(
+ dataTracingToAHookCall(
+ EP.fromString('sb/app:my-component:component-root'),
+ 'useArray',
+ dataPathNotPossible,
+ ),
+ )
})
it('Traces back a prop to a useLoaderData() hook through assignment indirections', async () => {
@@ -555,14 +625,14 @@ describe('Data Tracing', () => {
EPP.create(EP.fromString('sb/app:my-component:component-root'), PP.create('title')),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
dataTracingToAHookCall(
EP.fromString('sb/app:my-component:component-root'),
'useLoaderData',
- ['title'],
+ dataPathSuccess(['title']),
),
)
})
@@ -593,14 +663,14 @@ describe('Data Tracing', () => {
EPP.create(EP.fromString('sb/app:my-component:component-root'), PP.create('title')),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
dataTracingToAHookCall(
EP.fromString('sb/app:my-component:component-root'),
'useLoaderData',
- ['title'],
+ dataPathSuccess(['title']),
),
)
})
@@ -637,16 +707,15 @@ describe('Data Tracing', () => {
),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
- dataTracingToAHookCall(EP.fromString('sb/app:my-component'), 'useLoaderData', [
- 'very',
- 'deep',
- 'title',
- 'value',
- ]),
+ dataTracingToAHookCall(
+ EP.fromString('sb/app:my-component'),
+ 'useLoaderData',
+ dataPathSuccess(['very', 'deep', 'title', 'value']),
+ ),
)
})
@@ -683,16 +752,15 @@ describe('Data Tracing', () => {
),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
- dataTracingToAHookCall(EP.fromString('sb/app:my-component'), 'useLoaderData', [
- 'very',
- 'deep',
- 'title',
- 'value',
- ]),
+ dataTracingToAHookCall(
+ EP.fromString('sb/app:my-component'),
+ 'useLoaderData',
+ dataPathSuccess(['very', 'deep', 'title', 'value']),
+ ),
)
})
})
@@ -719,11 +787,15 @@ describe('Data Tracing', () => {
EPP.create(EP.fromString('sb/app:my-component:component-root'), PP.create('title')),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
- dataTracingToLiteralAttribute(EP.fromString('sb/app:my-component'), PP.create('title'), []),
+ dataTracingToLiteralAttribute(
+ EP.fromString('sb/app:my-component'),
+ PP.create('title'),
+ dataPathSuccess([]),
+ ),
)
})
@@ -748,11 +820,15 @@ describe('Data Tracing', () => {
EPP.create(EP.fromString('sb/app:my-component:component-root'), PP.create('title')),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
- dataTracingToLiteralAttribute(EP.fromString('sb/app:my-component'), PP.create('title'), []),
+ dataTracingToLiteralAttribute(
+ EP.fromString('sb/app:my-component'),
+ PP.create('title'),
+ dataPathSuccess([]),
+ ),
)
})
@@ -777,11 +853,15 @@ describe('Data Tracing', () => {
EPP.create(EP.fromString('sb/app:my-component:component-root'), PP.create('title')),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
- dataTracingToLiteralAttribute(EP.fromString('sb/app:my-component'), PP.create('title'), []),
+ dataTracingToLiteralAttribute(
+ EP.fromString('sb/app:my-component'),
+ PP.create('title'),
+ dataPathSuccess([]),
+ ),
)
})
})
@@ -819,13 +899,15 @@ describe('Data Tracing', () => {
),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
- dataTracingToLiteralAttribute(EP.fromString('sb/app:my-component'), PP.create('titles'), [
- '1',
- ]),
+ dataTracingToLiteralAttribute(
+ EP.fromString('sb/app:my-component'),
+ PP.create('titles'),
+ dataPathSuccess(['1']),
+ ),
)
})
@@ -866,14 +948,14 @@ describe('Data Tracing', () => {
),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
dataTracingToAHookCall(
EP.fromString('sb/app:my-component:component-root'),
'useLoaderData',
- ['reviews', '1', 'title'],
+ dataPathSuccess(['reviews', '1', 'title']),
),
)
})
@@ -915,14 +997,14 @@ describe('Data Tracing', () => {
),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
dataTracingToAHookCall(
EP.fromString('sb/app:my-component:component-root'),
'useLoaderData',
- ['reviews', '1', 'title'],
+ dataPathSuccess(['reviews', '1', 'title']),
),
)
})
@@ -974,16 +1056,15 @@ describe('Data Tracing', () => {
EP.fromString('sb/app:my-component:component-root:inner-component-root'),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
- dataTracingToAHookCall(EP.fromString('sb/app:my-component'), 'useLoaderData', [
- 'very',
- 'deep',
- 'title',
- 'value',
- ]),
+ dataTracingToAHookCall(
+ EP.fromString('sb/app:my-component'),
+ 'useLoaderData',
+ dataPathSuccess(['very', 'deep', 'title', 'value']),
+ ),
)
})
@@ -1035,15 +1116,15 @@ describe('Data Tracing', () => {
EP.fromString('sb/app:my-component:component-root:inner-component-root'),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
- dataTracingToAHookCall(EP.fromString('sb/app:my-component'), 'useLoaderData', [
- 'very',
- 'deep',
- 'name',
- ]),
+ dataTracingToAHookCall(
+ EP.fromString('sb/app:my-component'),
+ 'useLoaderData',
+ dataPathSuccess(['very', 'deep', 'name']),
+ ),
)
})
@@ -1081,14 +1162,14 @@ describe('Data Tracing', () => {
EP.fromString('sb/app:my-component:app-root'),
editor.getEditorState().editor.jsxMetadata,
editor.getEditorState().editor.projectContents,
- [],
+ dataPathSuccess([]),
)
expect(traceResult).toEqual(
dataTracingToElementAtScope(
EP.fromString('sb/app:my-component:app-root'),
mappedElement,
- [],
+ dataPathSuccess([]),
),
)
})
diff --git a/editor/src/core/data-tracing/data-tracing.ts b/editor/src/core/data-tracing/data-tracing.ts
index 82d5449aa3de..1659e2213151 100644
--- a/editor/src/core/data-tracing/data-tracing.ts
+++ b/editor/src/core/data-tracing/data-tracing.ts
@@ -34,17 +34,82 @@ import { assertNever } from '../shared/utils'
export type DataPath = Array
+export interface DataPathSuccess {
+ type: 'DATA_PATH_SUCCESS'
+ dataPath: DataPath
+}
+
+export function dataPathSuccess(dataPath: DataPath): DataPathSuccess {
+ return {
+ type: 'DATA_PATH_SUCCESS',
+ dataPath: dataPath,
+ }
+}
+
+export interface DataPathNotPossible {
+ type: 'DATA_PATH_NOT_POSSIBLE'
+}
+
+export const dataPathNotPossible: DataPathNotPossible = {
+ type: 'DATA_PATH_NOT_POSSIBLE',
+}
+
+export interface DataPathUnavailable {
+ type: 'DATA_PATH_UNAVAILABLE'
+}
+
+export const dataPathUnavailable: DataPathUnavailable = {
+ type: 'DATA_PATH_UNAVAILABLE',
+}
+
+export type DataPathPositiveResult = DataPathSuccess | DataPathNotPossible
+
+export function combinePositiveResults(
+ first: DataPathPositiveResult | DataPath,
+ second: DataPathPositiveResult | DataPath,
+): DataPathPositiveResult {
+ if (Array.isArray(first)) {
+ if (Array.isArray(second)) {
+ return dataPathSuccess([...first, ...second])
+ } else if (dataPathResultIsSuccess(second)) {
+ return dataPathSuccess([...first, ...second.dataPath])
+ }
+ } else if (dataPathResultIsSuccess(first)) {
+ if (Array.isArray(second)) {
+ return dataPathSuccess([...first.dataPath, ...second])
+ } else if (dataPathResultIsSuccess(second)) {
+ return dataPathSuccess([...first.dataPath, ...second.dataPath])
+ }
+ }
+
+ return dataPathNotPossible
+}
+
+export type DataPathResult = DataPathPositiveResult | DataPathUnavailable
+
+export function dataPathResultIsSuccess(result: DataPathResult): result is DataPathSuccess {
+ return result.type === 'DATA_PATH_SUCCESS'
+}
+
+export function dataPathResultIsNotPossible(result: DataPathResult): result is DataPathNotPossible {
+ return result.type === 'DATA_PATH_NOT_POSSIBLE'
+}
+
+export function dataPathResultIsUnavailable(result: DataPathResult): result is DataPathUnavailable {
+ return result.type === 'DATA_PATH_UNAVAILABLE'
+}
+
export type DataTracingToLiteralAttribute = {
type: 'literal-attribute'
elementPath: ElementPath
property: PropertyPath
- dataPathIntoAttribute: DataPath
+ dataPathIntoAttribute: DataPathPositiveResult
}
export function dataTracingToLiteralAttribute(
elementPath: ElementPath,
property: PropertyPath,
- dataPathIntoAttribute: DataPath,
+ dataPathIntoAttribute: DataPathPositiveResult,
): DataTracingToLiteralAttribute {
return {
type: 'literal-attribute',
@@ -58,13 +123,13 @@ export type DataTracingToElementAtScope = {
type: 'element-at-scope'
scope: ElementPath
element: JSXElementChild
- dataPathIntoAttribute: DataPath
+ dataPathIntoAttribute: DataPathPositiveResult
}
export function dataTracingToElementAtScope(
scope: ElementPath,
element: JSXElementChild,
- dataPathIntoAttribute: DataPath,
+ dataPathIntoAttribute: DataPathPositiveResult,
): DataTracingToElementAtScope {
return {
type: 'element-at-scope',
@@ -78,13 +143,13 @@ export type DataTracingToAHookCall = {
type: 'hook-result'
hookName: string
elementPath: ElementPath
- dataPathIntoAttribute: DataPath
+ dataPathIntoAttribute: DataPathPositiveResult
}
export function dataTracingToAHookCall(
elementPath: ElementPath,
hookName: string,
- dataPathIntoAttribute: DataPath,
+ dataPathIntoAttribute: DataPathPositiveResult,
): DataTracingToAHookCall {
return {
type: 'hook-result',
@@ -98,13 +163,13 @@ export type DataTracingToAComponentProp = {
type: 'component-prop'
elementPath: ElementPath
propertyPath: PropertyPath
- dataPathIntoAttribute: DataPath
+ dataPathIntoAttribute: DataPathPositiveResult
}
export function dataTracingToAComponentProp(
elementPath: ElementPath,
propertyPath: PropertyPath,
- dataPathIntoAttribute: DataPath,
+ dataPathIntoAttribute: DataPathPositiveResult,
): DataTracingToAComponentProp {
return {
type: 'component-prop',
@@ -193,8 +258,29 @@ function processJSPropertyAccessors(
function propUsedByIdentifierOrAccess(
param: Param,
originalIdentifier: JSIdentifier,
- pathDrillSoFar: DataPath,
-): Either {
+ pathDrillSoFar: DataPathPositiveResult,
+): Either {
+ function getPropertyNameFromPathSoFar(): Either<
+ string,
+ { propertyName: string; modifiedPathDrillSoFar: DataPathPositiveResult }
+ > {
+ switch (pathDrillSoFar.type) {
+ case 'DATA_PATH_SUCCESS':
+ const propertyName = pathDrillSoFar.dataPath.at(0)
+ if (propertyName == null) {
+ return left('Path so far is empty.')
+ } else {
+ return right({
+ propertyName: propertyName,
+ modifiedPathDrillSoFar: dataPathSuccess(pathDrillSoFar.dataPath.slice(1)),
+ })
+ }
+ case 'DATA_PATH_NOT_POSSIBLE':
+ return left('Path is not available.')
+ default:
+ assertNever(pathDrillSoFar)
+ }
+ }
switch (param.boundParam.type) {
case 'REGULAR_PARAM': {
// in case of a regular prop param, first we want to match the param name to the original identifier
@@ -202,20 +288,14 @@ function propUsedByIdentifierOrAccess(
return left('identifier does not match the prop name')
}
- return right({
- propertyName: pathDrillSoFar[0],
- modifiedPathDrillSoFar: pathDrillSoFar.slice(1),
- })
+ return getPropertyNameFromPathSoFar()
}
case 'DESTRUCTURED_OBJECT': {
for (const paramPart of param.boundParam.parts) {
if (paramPart.param.boundParam.type === 'REGULAR_PARAM') {
if (paramPart.param.boundParam.paramName === originalIdentifier.name) {
if (paramPart.param.dotDotDotToken) {
- return right({
- propertyName: pathDrillSoFar[0],
- modifiedPathDrillSoFar: pathDrillSoFar.slice(1),
- })
+ return getPropertyNameFromPathSoFar()
} else {
return right({
propertyName: paramPart.param.boundParam.paramName,
@@ -238,8 +318,8 @@ function propUsedByIdentifierOrAccess(
function paramUsedByIdentifierOrAccess(
param: Param,
originalIdentifier: JSIdentifier,
- pathDrillSoFar: DataPath,
-): Either {
+ pathDrillSoFar: DataPathPositiveResult,
+): Either {
switch (param.boundParam.type) {
case 'REGULAR_PARAM': {
// in case of a regular prop param, first we want to match the param name to the original identifier
@@ -261,7 +341,10 @@ function paramUsedByIdentifierOrAccess(
})
} else {
return right({
- modifiedPathDrillSoFar: [paramPart.param.boundParam.paramName, ...pathDrillSoFar],
+ modifiedPathDrillSoFar: combinePositiveResults(
+ [paramPart.param.boundParam.paramName],
+ pathDrillSoFar,
+ ),
})
}
}
@@ -282,7 +365,7 @@ export function traceDataFromElement(
enclosingScope: ElementPath, // <- the closest "parent" element path which points to the narrowest scope around the JSXElementChild. this is where we will start our upward scope walk
metadata: ElementInstanceMetadataMap,
projectContents: ProjectContentTreeRoot,
- pathDrillSoFar: Array,
+ pathDrillSoFar: DataPathPositiveResult,
): DataTracingResult {
switch (startFromElement.type) {
case 'JSX_ELEMENT':
@@ -293,7 +376,7 @@ export function traceDataFromElement(
case 'ATTRIBUTE_VALUE':
case 'ATTRIBUTE_NESTED_ARRAY':
case 'ATTRIBUTE_NESTED_OBJECT':
- return dataTracingToElementAtScope(enclosingScope, startFromElement, [])
+ return dataTracingToElementAtScope(enclosingScope, startFromElement, dataPathSuccess([]))
case 'JS_IDENTIFIER':
case 'JS_ELEMENT_ACCESS':
case 'JS_PROPERTY_ACCESS':
@@ -317,7 +400,7 @@ function traceDataFromIdentifierOrAccess(
enclosingScope: ElementPath,
metadata: ElementInstanceMetadataMap,
projectContents: ProjectContentTreeRoot,
- pathDrillSoFar: Array,
+ pathDrillSoFar: DataPathPositiveResult,
): DataTracingResult {
const componentHoldingElement: UtopiaJSXComponent | null = findContainingComponentForElementPath(
enclosingScope,
@@ -344,7 +427,7 @@ function traceDataFromIdentifierOrAccess(
EP.getPathOfComponentRoot(enclosingScope),
componentHoldingElement,
identifier,
- [...dataPath.value.path, ...pathDrillSoFar],
+ combinePositiveResults(dataPath.value.path, pathDrillSoFar),
)
}
@@ -352,7 +435,7 @@ export function traceDataFromProp(
startFrom: ElementPropertyPath,
metadata: ElementInstanceMetadataMap,
projectContents: ProjectContentTreeRoot,
- pathDrillSoFar: Array,
+ pathDrillSoFar: DataPathPositiveResult,
): DataTracingResult {
const elementHoldingProp = MetadataUtils.findElementByElementPath(metadata, startFrom.elementPath)
if (elementHoldingProp == null) {
@@ -413,7 +496,7 @@ function walkUpInnerScopesUntilReachingComponent(
containingComponentRootPath: ElementPath,
componentHoldingElement: UtopiaJSXComponent,
identifier: JSIdentifier,
- pathDrillSoFar: DataPath,
+ pathDrillSoFar: DataPathPositiveResult,
): DataTracingResult {
if (EP.pathsEqual(currentElementPathOfWalk, containingComponentRootPath)) {
const resultInComponentScope: DataTracingResult = lookupInComponentScope(
@@ -454,9 +537,11 @@ function walkUpInnerScopesUntilReachingComponent(
const param = mapFunction.params[0] // the map function's first param is the mapped value
if (param != null) {
// let's try to match the name to the containing component's props!
- const foundPropSameName = paramUsedByIdentifierOrAccess(param, identifier, [
- ...pathDrillSoFar,
- ])
+ const foundPropSameName = paramUsedByIdentifierOrAccess(
+ param,
+ identifier,
+ pathDrillSoFar,
+ )
if (isRight(foundPropSameName)) {
// ok, so now we need to figure out what the map is mapping over
@@ -489,11 +574,10 @@ function walkUpInnerScopesUntilReachingComponent(
containingComponentRootPath,
componentHoldingElement,
dataPath.value.originalIdentifier,
- [
- ...dataPath.value.path,
- mapIndexHack,
- ...foundPropSameName.value.modifiedPathDrillSoFar,
- ],
+ combinePositiveResults(
+ combinePositiveResults(dataPath.value.path, [mapIndexHack]),
+ foundPropSameName.value.modifiedPathDrillSoFar,
+ ),
)
}
}
@@ -547,65 +631,64 @@ function getPossibleHookCall(expression: JSExpression): string | null {
}
}
-function findPathToIdentifier(param: BoundParam, identifier: string): DataPath | null {
- function innerFindPath(workingParam: BoundParam, currentPath: DataPath): DataPath | null {
+function findPathToIdentifier(param: BoundParam, identifier: string): DataPathResult {
+ function innerFindPath(workingParam: BoundParam, currentPath: DataPath): DataPathResult {
switch (workingParam.type) {
case 'REGULAR_PARAM':
if (workingParam.paramName === identifier) {
- return [...currentPath, workingParam.paramName]
+ return dataPathSuccess([...currentPath, workingParam.paramName])
} else {
- return null
+ return dataPathUnavailable
}
case 'DESTRUCTURED_OBJECT':
for (const part of workingParam.parts) {
- // Prevent drilling down through spread values.
- if (part.param.dotDotDotToken) {
- return null
- } else if (part.propertyName == identifier) {
- return [...currentPath, part.propertyName]
+ if (part.propertyName == identifier) {
+ return dataPathSuccess([...currentPath, part.propertyName])
+ } else if (
+ isRegularParam(part.param.boundParam) &&
+ part.param.boundParam.paramName === identifier &&
+ part.param.dotDotDotToken
+ ) {
+ // Object spread here, we're jumping over the field name of the destructured object.
+ return dataPathSuccess(currentPath)
} else if (
- part.propertyName != null &&
isRegularParam(part.param.boundParam) &&
part.param.boundParam.paramName === identifier
) {
- return [...currentPath, part.propertyName]
- } else {
- const possibleResult = innerFindPath(part.param.boundParam, currentPath)
- if (possibleResult != null) {
- return possibleResult
- }
+ return dataPathSuccess([...currentPath, part.propertyName ?? identifier])
}
}
- break
+ return dataPathUnavailable
case 'DESTRUCTURED_ARRAY':
let arrayIndex: number = 0
for (const part of workingParam.parts) {
switch (part.type) {
case 'PARAM':
- // Prevent drilling down through spread values.
+ // For a spread we can still locate the origin, but the path is
+ // potentially gibberish.
if (part.dotDotDotToken) {
- return null
+ return dataPathNotPossible
}
const possibleResult = innerFindPath(part.boundParam, [
...currentPath,
`${arrayIndex}`,
])
- if (possibleResult != null) {
+ if (dataPathResultIsSuccess(possibleResult)) {
return possibleResult
}
break
case 'OMITTED_PARAM':
- return null
+ return dataPathUnavailable
default:
assertNever(part)
}
arrayIndex++
}
- return null
+ return dataPathUnavailable
default:
assertNever(workingParam)
}
- return null
+ return dataPathUnavailable
}
return innerFindPath(param, [])
}
@@ -615,7 +698,7 @@ function lookupInComponentScope(
componentPath: ElementPath,
componentHoldingElement: UtopiaJSXComponent,
originalIdentifier: JSIdentifier,
- pathDrillSoFar: DataPath,
+ pathDrillSoFar: DataPathPositiveResult,
): DataTracingResult {
const identifier = originalIdentifier
@@ -626,7 +709,7 @@ function lookupInComponentScope(
const foundPropSameName = propUsedByIdentifierOrAccess(
componentHoldingElement.param,
identifier,
- [...pathDrillSoFar],
+ pathDrillSoFar,
)
if (isRight(foundPropSameName)) {
@@ -676,7 +759,7 @@ function lookupInComponentScope(
componentPath,
componentHoldingElement,
dataPath.value.originalIdentifier,
- [...dataPath.value.path, ...pathDrillSoFar],
+ combinePositiveResults(dataPath.value.path, pathDrillSoFar),
)
}
}
@@ -691,7 +774,7 @@ function lookupInComponentScope(
return dataTracingToAHookCall(
componentPath,
foundAssignmentOfIdentifier.rightHandSide.originalJavascript.split('()')[0],
- [...pathDrillSoFar],
+ pathDrillSoFar,
)
}
}
@@ -709,7 +792,7 @@ function lookupInComponentScope(
identifier.name,
)
const originExpression = assignment.rightHandSide
- if (possiblePathToIdentifier == null) {
+ if (dataPathResultIsUnavailable(possiblePathToIdentifier)) {
return null
} else {
const possibleHookCall = getPossibleHookCall(assignment.rightHandSide)
@@ -719,13 +802,14 @@ function lookupInComponentScope(
componentPath,
componentHoldingElement,
originExpression,
- [...possiblePathToIdentifier, ...pathDrillSoFar],
+ combinePositiveResults(possiblePathToIdentifier, pathDrillSoFar),
)
} else if (possibleHookCall != null) {
- return dataTracingToAHookCall(componentPath, possibleHookCall, [
- ...possiblePathToIdentifier,
- ...pathDrillSoFar,
- ])
+ return dataTracingToAHookCall(
+ componentPath,
+ possibleHookCall,
+ combinePositiveResults(possiblePathToIdentifier, pathDrillSoFar),
+ )
}
}
}