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), + ) } } }