diff --git a/ROADMAP.md b/ROADMAP.md index 3352473..68576d4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -14,6 +14,7 @@ Type: `Feature` - `Bug` - `Enhancement` - `Refactor` - `Unknown` - [ ] `Enhancement` Skip memoization of useState setter functions - [ ] `Enhancement` When unwrapping array and object patterns, optimize the unwrapping by limiting it to component variables - [ ] `Enhancement` Support React.createElement calls +- [ ] `Enhancement` Hot module reloading improvement utilizing a checksum to invalidate cache - [ ] `Feature` Memoize array map items ## `Feature` ESLint Plugin @@ -30,3 +31,4 @@ The following are unknowns that need to be researched and tested. - [ ] Assignment expressions in JSX `
{(a = 1)} {a}
` - is the order of execution guaranteed? - [ ] Source maps - is it possible to generate source maps for the transformed code? - [ ] Hot reloading - how does hot reloading work with the transformed code? +- [ ] Memoization of values declared after the first return statement diff --git a/packages/compiler/package.json b/packages/compiler/package.json index abf84e8..e86801c 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -21,7 +21,7 @@ "dev": "yarn build --watch", "build": "tsup src/main.ts --dts --format cjs,esm", "test": "jest", - "lint": "eslint src/**/* --max-warnings 10" + "lint": "eslint src/**/* --max-warnings 15" }, "devDependencies": { "@babel/plugin-syntax-jsx": "^7.23.3", diff --git a/packages/compiler/src/ast-factories/convert-statement-to-segment-callable.ts b/packages/compiler/src/ast-factories/convert-statement-to-segment-callable.ts new file mode 100644 index 0000000..d5e719d --- /dev/null +++ b/packages/compiler/src/ast-factories/convert-statement-to-segment-callable.ts @@ -0,0 +1,75 @@ +import * as t from "@babel/types"; +import { DEFAULT_SEGMENT_CALLABLE_VARIABLE_NAME } from "~/utils/constants"; + +export function convertStatementToSegmentCallable( + statement: babel.NodePath, + { + initialValue, + cacheNullValue, + segmentCallableId = statement.scope.generateUidIdentifier( + DEFAULT_SEGMENT_CALLABLE_VARIABLE_NAME + ), + }: { + initialValue?: t.Expression; + performReplacement?: boolean; + cacheNullValue?: t.Expression; + segmentCallableId?: t.Identifier; + } +) { + const parentDeclaration = statement.find((p) => + p.isVariableDeclaration() + ) as babel.NodePath | null; + + const makeSegmentCallable = (statements: t.Statement[]) => { + return t.variableDeclaration("const", [ + t.variableDeclarator( + segmentCallableId, + t.arrowFunctionExpression( + [], + t.blockStatement( + statements.concat( + cacheNullValue ? [t.returnStatement(cacheNullValue)] : [] + ) + ) + ) + ), + ]); + }; + + let replacements: t.Node[] | null = null; + if (parentDeclaration) { + const newKind = parentDeclaration.node.kind === "var" ? "var" : "let"; + const newDeclaration = t.variableDeclaration( + newKind, + parentDeclaration.node.declarations.map((declaration) => { + return t.variableDeclarator(declaration.id, initialValue); + }) + ); + + const assignmentExpressionStatements = parentDeclaration.node.declarations + .map((declarator) => { + return declarator.init + ? t.expressionStatement( + t.assignmentExpression("=", declarator.id, declarator.init) + ) + : null; + }) + .filter((v): v is t.ExpressionStatement => Boolean(v)); + + replacements = [ + newDeclaration, + makeSegmentCallable(assignmentExpressionStatements), + ]; + } else { + replacements = [makeSegmentCallable([statement.node])]; + } + + const prformTransformation = () => + replacements ? statement.replaceWithMultiple(replacements) : null; + + return { + segmentCallableId, + replacements, + prformTransformation, + }; +} diff --git a/packages/compiler/src/ast-factories/make-dependency-condition.ts b/packages/compiler/src/ast-factories/make-dependency-condition.ts index f104633..24728ef 100644 --- a/packages/compiler/src/ast-factories/make-dependency-condition.ts +++ b/packages/compiler/src/ast-factories/make-dependency-condition.ts @@ -1,95 +1,48 @@ import * as t from "@babel/types"; -import { ComponentVariable } from "~/classes/ComponentVariable"; -import { isReferenceIdentifier } from "~/utils/is-reference-identifier"; -import { memberExpressionToDotNotation } from "~/utils/member-expression-to-dot-notation"; - -export function makeDependencyCondition(componentVariable: ComponentVariable) { - const dependencies = componentVariable.getDependencies(); - const isNotSetCondition = - componentVariable.getCacheIsNotSetAccessExpression(); - - const comparisonsMap = new Map< - string, - [babel.types.Expression, babel.types.Expression] +import { ComponentMutableSegment } from "~/classes/ComponentMutableSegment"; + +export function makeDependencyCondition( + mutableSegment: ComponentMutableSegment +): t.Expression | null { + const dependencies = mutableSegment.getDependencies(); + const isNotSetCondition: t.Expression | null = + mutableSegment.isComponentVariable() + ? mutableSegment.getCacheIsNotSetAccessExpression() + : null; + + const comparisonTuples = new Set< + [left: babel.types.Expression, right: babel.types.Expression] >(); - dependencies.forEach((dependency) => { - const dependencyId = t.identifier(dependency.name); - - // When the variable is used in a member expression, we should optimize comparisons to the last member of member expression as well - const path = componentVariable.binding.path; - const parentPath = path.find((p) => p.isStatement()); - - const references = new Set>(); - - parentPath?.traverse({ - Identifier(innerPath) { - if (isReferenceIdentifier(innerPath)) { - references.add(innerPath); - } - }, - }); - - references.forEach((reference) => { - const refParent = reference.parentPath; - const actualDependencyForThisReference = - componentVariable.component.getComponentVariable(reference.node.name); - - // The reference is not a component variable - if (!actualDependencyForThisReference) { - return; - } - - const dependencyCacheValueAccessor = - actualDependencyForThisReference.getCacheValueAccessExpression(); - - if (refParent.isMemberExpression()) { - const memberObject = refParent.get("object"); - if ( - memberObject.isIdentifier() && - memberObject.node.name === dependencyId.name - ) { - const makeMemberExpressionForCheck = (id: babel.types.Expression) => - t.memberExpression( - id, - refParent.node.property, - refParent.node.computed - ); - - const referenceMemberExpression = makeMemberExpressionForCheck( - reference.node - ); - const id = memberExpressionToDotNotation(referenceMemberExpression); - - if (!comparisonsMap.has(id)) { - const cacheMemberExpression = makeMemberExpressionForCheck( - dependencyCacheValueAccessor - ); - comparisonsMap.set(id, [ - referenceMemberExpression, - cacheMemberExpression, - ]); - } - } - } else { - const id = memberExpressionToDotNotation(reference.node); - - if (!comparisonsMap.has(id)) { - comparisonsMap.set(id, [ - reference.node, - dependencyCacheValueAccessor, - ]); - } - } - }); + const allDependencies = [...dependencies.values()]; + + allDependencies.forEach((dependency) => { + const dependencyId = dependency.componentVariable + ? t.identifier(dependency.componentVariable.name) + : null; + + if (!dependencyId) { + return; + } + + comparisonTuples.add([ + dependency.getMemberExpression( + t.identifier(dependency.componentVariable.name) + ), + dependency.getMemberExpression( + dependency.componentVariable.getCacheValueAccessExpression() + ), + ]); }); - return Array.from(comparisonsMap.values()).reduce( + return Array.from(comparisonTuples.values()).reduce( (condition, [left, right]) => { const binaryExpression = t.binaryExpression("!==", left, right); - return t.logicalExpression("||", condition, binaryExpression); + return condition + ? t.logicalExpression("||", condition, binaryExpression) + : binaryExpression; }, - isNotSetCondition as babel.types.Expression + isNotSetCondition as babel.types.Expression | null ); } diff --git a/packages/compiler/src/ast-factories/make-unwrapped-declarations.ts b/packages/compiler/src/ast-factories/make-unwrapped-declarations.ts index 3668ba6..f816019 100644 --- a/packages/compiler/src/ast-factories/make-unwrapped-declarations.ts +++ b/packages/compiler/src/ast-factories/make-unwrapped-declarations.ts @@ -10,9 +10,14 @@ export function makeUnwrappedDeclarations( const unwrappedEntries = unwrapPatternAssignment(id, tempVariableId); const unwrappedDeclarations = unwrappedEntries.map((entry) => { - return t.variableDeclaration(kind, [ - t.variableDeclarator(entry.id, entry.value), - ]); + const binding = id.scope.getBinding(entry.name); + + return [ + t.variableDeclaration(kind, [ + t.variableDeclarator(entry.id, entry.value), + ]), + binding, + ] as const; }); return { diff --git a/packages/compiler/src/classes/Component.ts b/packages/compiler/src/classes/Component.ts index 77f271b..a2fc103 100644 --- a/packages/compiler/src/classes/Component.ts +++ b/packages/compiler/src/classes/Component.ts @@ -3,20 +3,34 @@ import { Binding } from "@babel/traverse"; import * as t from "@babel/types"; import { DEFAULT_CACHE_COMMIT_VARIABLE_NAME, + DEFAULT_CACHE_NULL_VARIABLE_NAME, DEFAULT_CACHE_VARIABLE_NAME, RUNTIME_MODULE_CREATE_CACHE_HOOK_NAME, } from "~/utils/constants"; -import { getReferencedVariablesInside } from "~/utils/get-referenced-variables-inside"; -import { getReturnsOfFunction } from "~/utils/get-returns-of-function"; +import { isVariableInScopeOf } from "~/utils/is-variable-in-scope-of"; +import { unwrapJsxElements } from "~/utils/unwrap-jsx-elements"; +import { unwrapJsxExpressions } from "~/utils/unwrap-jsx-expressions"; import { getFunctionParent } from "../utils/get-function-parent"; +import { ComponentMutableSegment } from "./ComponentMutableSegment"; +import { ComponentRunnableSegment } from "./ComponentRunnableSegment"; import { ComponentVariable } from "./ComponentVariable"; -import { unwrapJsxExpressions } from "~/utils/unwrap-jsx-expressions"; -import { unwrapJsxElements } from "~/utils/unwrap-jsx-elements"; export class Component { - private componentVariables = new Map(); + private runnableSegments = new Map< + babel.NodePath, + ComponentRunnableSegment + >(); + private componentVariables = new Map(); private cacheValueIdentifier: t.Identifier; private cacheCommitIdentifier: t.Identifier; + private cacheNullIdentifier: t.Identifier; + + private rootSegment: ComponentRunnableSegment | null = null; + + private mapBlockStatementToComponentRunnableSegment = new Map< + babel.NodePath, + ComponentRunnableSegment + >(); constructor(public path: babel.NodePath) { path.assertFunction(); @@ -28,16 +42,22 @@ export class Component { this.cacheCommitIdentifier = path.scope.generateUidIdentifier( DEFAULT_CACHE_COMMIT_VARIABLE_NAME ); + + this.cacheNullIdentifier = path.scope.generateUidIdentifier( + DEFAULT_CACHE_NULL_VARIABLE_NAME + ); } - computeComponentVariables() { + computeComponentSegments() { this.prepareComponentBody(); - getReturnsOfFunction(this.path).forEach((returnPath) => { - const bindings = getReferencedVariablesInside(returnPath); - bindings.forEach((binding) => { - this.addComponentVariable(binding); - }); - }); + + const body = this.path.get("body"); + if (!body.isBlockStatement()) { + return; + } + + this.rootSegment = this.addRunnableSegment(body); + this.rootSegment.computeDependencyGraph(); } prepareComponentBody() { @@ -45,48 +65,92 @@ export class Component { unwrapJsxElements(this.path); } - hasComponentVariable(name: string) { - return this.componentVariables.has(name); + hasComponentVariable(binding: Binding) { + return this.componentVariables.has(binding); } - getComponentVariable(name: string) { - return this.componentVariables.get(name); + getComponentVariable(binding: Binding) { + return this.componentVariables.get(binding); } - addComponentVariable(binding: Binding) { - const path = binding.path; + private findBlockStatementOfPath(path: babel.NodePath) { + return path.findParent( + (innerPath) => + innerPath.isBlockStatement() && innerPath.isDescendant(this.path) + ) as babel.NodePath | null; + } + addComponentVariable(binding: Binding) { // If the binding is not in the same function, ignore it i.e. it can't be a component variable - if (!this.isTheFunctionParentOf(path)) { + if (binding.scope !== this.path.scope) { return null; } - const name = binding.identifier.name; + const { path } = binding; - if (this.hasComponentVariable(name)) { - return this.getComponentVariable(name); + const blockStatement = this.findBlockStatementOfPath(path); + const parent = blockStatement + ? this.mapBlockStatementToComponentRunnableSegment.get(blockStatement) ?? + null + : null; + + if (this.hasComponentVariable(binding)) { + const componentVariable = this.getComponentVariable(binding)!; + componentVariable.setParent(parent); + return componentVariable; } const componentVariable = new ComponentVariable( - binding, this, + parent, + binding, this.componentVariables.size ); - this.componentVariables.set(name, componentVariable); + this.componentVariables.set(binding, componentVariable); + componentVariable.unwrapAssignmentPatterns(); componentVariable.computeDependencyGraph(); return componentVariable; } + addRunnableSegment(path: babel.NodePath) { + const blockStatement = this.findBlockStatementOfPath(path); + const parent = blockStatement + ? this.mapBlockStatementToComponentRunnableSegment.get(blockStatement) ?? + null + : null; + + if (this.runnableSegments.has(path)) { + const found = this.runnableSegments.get(path)!; + found.setParent(parent); + return found; + } + + const runnableSegment = new ComponentRunnableSegment(this, parent, path); + + if (path.isBlockStatement()) { + this.mapBlockStatementToComponentRunnableSegment.set( + path, + runnableSegment + ); + } + + this.runnableSegments.set(path, runnableSegment); + + runnableSegment.computeDependencyGraph(); + + return runnableSegment; + } + isTheFunctionParentOf(path: babel.NodePath) { return getFunctionParent(path) === this.path; } getRootComponentVariables() { return [...this.componentVariables.values()].filter( - (componentVariable) => !componentVariable.isDerived() + (componentVariable) => !componentVariable.hasDependencies() ); } @@ -98,26 +162,56 @@ export class Component { return t.cloneNode(this.cacheCommitIdentifier); } - applyModification() { + getCacheNullIdentifier() { + return t.cloneNode(this.cacheNullIdentifier); + } + + private statementsToMutableSegmentMapCache: Map< + babel.NodePath, + ComponentMutableSegment + > | null = null; + + getStatementsToMutableSegmentMap() { + if (this.statementsToMutableSegmentMapCache) { + return this.statementsToMutableSegmentMapCache; + } + + const statementsToMutableSegmentMap = new Map< + babel.NodePath, + ComponentMutableSegment + >(); + + const statementsMapSet = (segment: ComponentMutableSegment) => { + const parent = segment.getParentStatement(); + if (parent) { + statementsToMutableSegmentMap.set(parent, segment); + } + }; + + this.runnableSegments.forEach(statementsMapSet); + this.componentVariables.forEach(statementsMapSet); + + this.statementsToMutableSegmentMapCache = statementsToMutableSegmentMap; + + return statementsToMutableSegmentMap; + } + + applyTransformation() { const cacheVariableDeclaration = this.makeCacheVariableDeclaration(); - const body = this.path.get("body"); + if (!this.rootSegment) { + throw new Error("Root segment not found"); + } - this.componentVariables.forEach((componentVariable) => { - componentVariable.applyModification(); - }); + const body = this.path.get("body"); - if (body.isBlockStatement()) { - body.unshiftContainer("body", cacheVariableDeclaration); + if (!body.isBlockStatement()) { + return; } - const returns = getReturnsOfFunction(this.path); + this.rootSegment.applyTransformation(); - returns.forEach((returnPath) => { - returnPath.insertBefore( - t.expressionStatement(t.callExpression(this.cacheCommitIdentifier, [])) - ); - }); + body.unshiftContainer("body", cacheVariableDeclaration); } getFunctionBlockStatement(): babel.NodePath | null { @@ -142,7 +236,11 @@ export class Component { const sizeNumber = t.numericLiteral(this.componentVariables.size); const declaration = t.variableDeclaration("const", [ t.variableDeclarator( - t.arrayPattern([this.cacheValueIdentifier, this.cacheCommitIdentifier]), + t.arrayPattern([ + this.cacheValueIdentifier, + this.cacheCommitIdentifier, + this.cacheNullIdentifier, + ]), t.callExpression(t.identifier(RUNTIME_MODULE_CREATE_CACHE_HOOK_NAME), [ sizeNumber, ]) @@ -168,4 +266,8 @@ export class Component { __debug_getComponentVariables() { return this.componentVariables; } + + isBindingInComponentScope(binding: Binding) { + return isVariableInScopeOf(binding, this.path.scope); + } } diff --git a/packages/compiler/src/classes/ComponentMutableSegment.ts b/packages/compiler/src/classes/ComponentMutableSegment.ts new file mode 100644 index 0000000..d19d421 --- /dev/null +++ b/packages/compiler/src/classes/ComponentMutableSegment.ts @@ -0,0 +1,200 @@ +import type * as babel from "@babel/core"; +import * as t from "@babel/types"; +import { makeDependencyCondition } from "~/ast-factories/make-dependency-condition"; +import { DEFAULT_SEGMENT_CALLABLE_VARIABLE_NAME } from "~/utils/constants"; +import { hasHookCall } from "~/utils/is-hook-call"; +import { Component } from "./Component"; +import type { ComponentRunnableSegment } from "./ComponentRunnableSegment"; +import { ComponentSegmentDependency } from "./ComponentSegmentDependency"; +import type { ComponentVariable } from "./ComponentVariable"; + +export const COMPONENT_MUTABLE_SEGMENT_COMPONENT_UNSET_TYPE = "Unset"; +export const COMPONENT_MUTABLE_SEGMENT_COMPONENT_VARIABLE_TYPE = + "ComponentVariable"; +export const COMPONENT_MUTABLE_SEGMENT_COMPONENT_RUNNABLE_SEGMENT_TYPE = + "ComponentRunnableSegment"; + +type ComponentMutableSegmentType = + | typeof COMPONENT_MUTABLE_SEGMENT_COMPONENT_UNSET_TYPE + | typeof COMPONENT_MUTABLE_SEGMENT_COMPONENT_VARIABLE_TYPE + | typeof COMPONENT_MUTABLE_SEGMENT_COMPONENT_RUNNABLE_SEGMENT_TYPE; + +export type SegmentTransformationResult = { + prformTransformation: () => babel.NodePath[] | null; + segmentCallableId: babel.types.Identifier; + dependencyConditions: babel.types.Expression | null; + hasHookCall: boolean; + hasReturnStatement?: boolean; + updateCache?: babel.types.Statement | null; + replacements: babel.types.Node[] | null; +} | null; + +export abstract class ComponentMutableSegment { + private segmentCallableId: t.Identifier | null = null; + protected appliedTransformation = false; + + // Mutable code segments that this depends on this + protected dependencies = new Set(); + + // Mutable code segments that are children of this + protected children = new Set(); + + protected parent: ComponentMutableSegment | null = null; + + constructor( + public component: Component, + parent: ComponentMutableSegment | null = null, + protected type: ComponentMutableSegmentType = COMPONENT_MUTABLE_SEGMENT_COMPONENT_UNSET_TYPE + ) { + if (parent) { + this.setParent(parent); + } + } + + setParent(parent: ComponentMutableSegment | null) { + this.parent?.removeChild(this); + this.parent = parent; + parent?.addChild(this); + } + + removeChild(child: ComponentMutableSegment) { + this.children.delete(child); + } + + addChild(child: ComponentMutableSegment) { + this.children.add(child); + } + + addDependency(componentVariable: ComponentVariable, accessorNode: t.Node) { + if (this.isComponentVariable() && componentVariable === this) { + return; + } + const componentSegmentDependency = new ComponentSegmentDependency( + componentVariable, + accessorNode as any + ); + + let alreadyHasDependency = false; + for (const dependency of this.dependencies) { + if (dependency.equals(componentSegmentDependency)) { + alreadyHasDependency = true; + break; + } + } + + if (!alreadyHasDependency) { + this.dependencies.add(componentSegmentDependency); + } + } + + hasDependencies() { + return this.dependencies.size > 0; + } + + getDependencies() { + return this.dependencies; + } + + abstract computeDependencyGraph(): void; + + abstract get path(): babel.NodePath; + + abstract applyTransformation( + performReplacement?: boolean + ): SegmentTransformationResult; + + protected makeDependencyCondition() { + return makeDependencyCondition(this); + } + + hasHookCall() { + return hasHookCall(this.path, this.component.path); + } + + getParentStatement() { + const parentStatement = this.path.find( + (p) => p.isStatement() && p.parentPath.isBlockStatement() + ) as babel.NodePath | null; + + return parentStatement; + } + + isComponentVariable(): this is ComponentVariable { + return this.type === COMPONENT_MUTABLE_SEGMENT_COMPONENT_VARIABLE_TYPE; + } + + isComponentRunnableSegment(): this is ComponentRunnableSegment { + return ( + this.type === COMPONENT_MUTABLE_SEGMENT_COMPONENT_RUNNABLE_SEGMENT_TYPE + ); + } + + protected makeSegmentCallStatement( + transformation: SegmentTransformationResult + ) { + if (!transformation) { + return null; + } + + const { + dependencyConditions, + segmentCallableId, + hasHookCall, + updateCache, + hasReturnStatement, + } = transformation; + + const callSegmentCallable = t.callExpression(segmentCallableId, []); + const updateStatements: t.Statement[] = []; + + if (hasReturnStatement) { + const customCallVariable = + this.component.path.scope.generateUidIdentifier(); + updateStatements.push( + t.variableDeclaration("const", [ + t.variableDeclarator(customCallVariable, callSegmentCallable), + ]) + ); + + updateStatements.push( + // if customCallVariable not equal to null, return it + t.ifStatement( + t.binaryExpression( + "!==", + customCallVariable, + this.component.getCacheNullIdentifier() + ), + t.blockStatement([t.returnStatement(customCallVariable)]) + ) + ); + } else { + updateStatements.push(t.expressionStatement(callSegmentCallable)); + } + + if (updateCache) { + updateStatements.push(updateCache); + } + + const callStatementWithCondition = + dependencyConditions && !hasHookCall + ? [ + t.ifStatement( + dependencyConditions, + t.blockStatement(updateStatements) + ), + ] + : updateStatements; + + return callStatementWithCondition; + } + + protected getSegmentCallableId() { + if (!this.segmentCallableId) { + this.segmentCallableId = this.component.path.scope.generateUidIdentifier( + DEFAULT_SEGMENT_CALLABLE_VARIABLE_NAME + ); + } + + return this.segmentCallableId; + } +} diff --git a/packages/compiler/src/classes/ComponentRunnableSegment.ts b/packages/compiler/src/classes/ComponentRunnableSegment.ts new file mode 100644 index 0000000..953d82c --- /dev/null +++ b/packages/compiler/src/classes/ComponentRunnableSegment.ts @@ -0,0 +1,165 @@ +import * as babel from "@babel/core"; +import { Binding } from "@babel/traverse"; +import { convertStatementToSegmentCallable } from "~/ast-factories/convert-statement-to-segment-callable"; +import { getBlockStatementsOfPath } from "~/utils/get-block-statements-of-path"; +import { getReferencedVariablesInside } from "~/utils/get-referenced-variables-inside"; +import { reorderByTopology } from "~/utils/reorder-by-topology"; +import { Component } from "./Component"; +import { + ComponentMutableSegment, + SegmentTransformationResult, +} from "./ComponentMutableSegment"; + +export class ComponentRunnableSegment extends ComponentMutableSegment { + private mapOfReturnStatementToReferencedBindings = new Map< + babel.NodePath, + Binding[] + >(); + + private blockReturnStatement: babel.NodePath | null = + null; + + constructor( + component: Component, + parent: ComponentMutableSegment | null = null, + private statement: babel.NodePath + ) { + super(component, parent, "ComponentRunnableSegment"); + } + + isRoot() { + return this.parent === null; + } + + get hasReturnStatement() { + return !!this.blockReturnStatement; + } + + computeDependencyGraph() { + this.dependencies.clear(); + + const path = this.path; + + const blockStatementChildren = getBlockStatementsOfPath(path); + + blockStatementChildren.forEach((blockStatement) => { + this.component.addRunnableSegment(blockStatement); + }); + + blockStatementChildren.forEach((currentPath) => { + const statements = currentPath.get("body"); + + if (path.isBlockStatement() && currentPath === path) { + statements.forEach((statement) => { + const returnStatement = statement; + if (this.hasReturnStatement) { + return; + } + + if (returnStatement.isReturnStatement()) { + this.blockReturnStatement = returnStatement; + + const bindings: Binding[] = []; + getReferencedVariablesInside(returnStatement).forEach((binding) => { + bindings.push(binding); + + this.component.addComponentVariable(binding); + }); + + this.mapOfReturnStatementToReferencedBindings.set( + returnStatement, + bindings + ); + } else { + this.component.addRunnableSegment(statement); + } + }); + } else { + const child = this.component.addRunnableSegment(currentPath); + this.children.add(child); + } + }); + } + + applyTransformation(performReplacement = true) { + const path = this.path; + + const transformationsToPerform: (() => babel.NodePath[] | null)[] = []; + const callables: babel.types.Statement[] = []; + + if (path.isBlockStatement()) { + const statements = path.get("body"); + + const segmentsMap = this.component.getStatementsToMutableSegmentMap(); + + const reorderedStatements = reorderByTopology(statements, segmentsMap); + + reorderedStatements.forEach((statement) => { + const segment = segmentsMap.get(statement); + + const transformation = segment?.applyTransformation(); + if (transformation) { + const callStatement = this.makeSegmentCallStatement(transformation); + + transformationsToPerform.push(() => + transformation.prformTransformation() + ); + + if (callStatement) { + callables.push(...callStatement); + } + } else if (statement) { + callables.push(statement.node); + } + }); + + this.children.forEach((child) => { + child.applyTransformation(); + }); + + const newNodes = transformationsToPerform + .flatMap((transformation) => transformation()?.map(({ node }) => node)) + .filter((v): v is babel.types.Statement => v !== null) + .concat(callables); + + path.node.body = newNodes; + + return null; + } + + const anyChildrenHasReturnStatement = Array.from(this.children).some( + (child) => child.isComponentRunnableSegment() && child.hasReturnStatement + ); + + const hasHookCall = this.hasHookCall(); + + const dependencyConditions = this.makeDependencyCondition(); + + const { prformTransformation, segmentCallableId, replacements } = + convertStatementToSegmentCallable(path, { + performReplacement, + segmentCallableId: this.getSegmentCallableId(), + }); + + return { + dependencyConditions, + hasHookCall, + prformTransformation, + segmentCallableId, + replacements, + hasReturnStatement: anyChildrenHasReturnStatement, + } satisfies SegmentTransformationResult; + } + + getParentStatement() { + return this.path; + } + + getBlockStatements() { + return getBlockStatementsOfPath(this.path); + } + + get path() { + return this.statement; + } +} diff --git a/packages/compiler/src/classes/ComponentSegmentDependency.ts b/packages/compiler/src/classes/ComponentSegmentDependency.ts new file mode 100644 index 0000000..3299f20 --- /dev/null +++ b/packages/compiler/src/classes/ComponentSegmentDependency.ts @@ -0,0 +1,150 @@ +import * as t from "@babel/types"; +import { ComponentVariable } from "./ComponentVariable"; + +type AccessorNode = + | t.MemberExpression + | t.OptionalMemberExpression + | t.Identifier + | t.PrivateName; + +export class AccessChainItem { + public nextComputed = false; + public right?: AccessChainItem = undefined; + + constructor( + public id: string, + public idExpression: t.Expression + ) {} + + toString() { + let currentString = this.id; + if (this.right) { + currentString += this.nextComputed + ? `[${this.right.toString()}]` + : `.${this.right.toString()}`; + } + return currentString; + } +} + +function isAccessorNode(node: t.Node): node is AccessorNode { + return ( + t.isMemberExpression(node) || + t.isOptionalMemberExpression(node) || + t.isIdentifier(node) || + t.isPrivateName(node) + ); +} + +// When the variable is used in a member expression, we should optimize comparisons to the last member of member expression as well +export class ComponentSegmentDependency { + private root: AccessChainItem; + + constructor( + public componentVariable: ComponentVariable, + public accessorNode: AccessorNode + ) { + let currentAccessChainItem: AccessChainItem | null = null; + + // Start from the root of member expression + let currentAccessorNode: AccessorNode | null = accessorNode; + + do { + let newAccessChainItem: AccessChainItem | null = null; + + if ( + t.isMemberExpression(currentAccessorNode) || + t.isOptionalMemberExpression(currentAccessorNode) + ) { + if (currentAccessChainItem) { + currentAccessChainItem.nextComputed = currentAccessorNode.computed; + } + + const property = currentAccessorNode.property; + let name = ""; + + if (t.isIdentifier(property)) { + name = property.name; + } else if (t.isStringLiteral(property)) { + name = property.value; + } else if (t.isNumericLiteral(property)) { + name = property.value.toString(); + } else if (t.isPrivateName(property)) { + name = "#" + property.id.name; + } else if (t.isBigIntLiteral(property)) { + name = property.value + "n"; + } else if ( + t.isTemplateLiteral(property) && + property.expressions.length === 0 && + property.quasis.length === 1 + ) { + name = property.quasis[0]!.value.raw; + } + + if (name) { + newAccessChainItem = new AccessChainItem(name, currentAccessorNode); + + if (isAccessorNode(currentAccessorNode.object)) { + currentAccessorNode = currentAccessorNode.object; + } else { + currentAccessorNode = null; + } + } else { + currentAccessorNode = null; + } + } else if (t.isIdentifier(currentAccessorNode)) { + newAccessChainItem = new AccessChainItem( + currentAccessorNode.name, + currentAccessorNode + ); + + currentAccessorNode = null; + } + + if (newAccessChainItem) { + if (currentAccessChainItem) { + newAccessChainItem.right = currentAccessChainItem; + } + } + + currentAccessChainItem = newAccessChainItem; + } while (currentAccessorNode); + + this.root = currentAccessChainItem!; + } + + getRootDependencyLink() { + return this.root; + } + + stringify() { + return this.root.toString(); + } + + equals(other: ComponentSegmentDependency) { + return ( + this.componentVariable === other.componentVariable && + this.stringify() === other.stringify() + ); + } + + getMemberExpression(replacementId: t.Expression) { + let endOfChaing: AccessChainItem | null = this.root; + + while (endOfChaing.right) { + endOfChaing = endOfChaing.right; + } + + const endOfChainExpression = t.cloneNode(endOfChaing.idExpression); + + if ( + t.isMemberExpression(endOfChainExpression) || + t.isOptionalMemberExpression(endOfChainExpression) + ) { + endOfChainExpression.object = replacementId; + return endOfChainExpression; + } + + return replacementId; + } +} diff --git a/packages/compiler/src/classes/ComponentVariable.ts b/packages/compiler/src/classes/ComponentVariable.ts index 438c5ec..8ffb5c4 100644 --- a/packages/compiler/src/classes/ComponentVariable.ts +++ b/packages/compiler/src/classes/ComponentVariable.ts @@ -1,40 +1,34 @@ import type * as babel from "@babel/core"; import { Binding } from "@babel/traverse"; import * as t from "@babel/types"; +import { convertStatementToSegmentCallable } from "~/ast-factories/convert-statement-to-segment-callable"; import { makeCacheEnqueueCallStatement } from "~/ast-factories/make-cache-enqueue-call-statement"; -import { makeDependencyCondition } from "~/ast-factories/make-dependency-condition"; import { makeUnwrappedDeclarations } from "~/ast-factories/make-unwrapped-declarations"; -import { variableDeclarationToAssignment } from "~/ast-factories/variable-declaration-to-assignment"; import { + DEFAULT_SEGMENT_CALLABLE_VARIABLE_NAME, + DEFAULT_UNWRAPPED_PROPS_VARIABLE_NAME, DEFAULT_UNWRAPPED_VARIABLE_NAME, RUNTIME_MODULE_CACHE_IS_NOT_SET_PROP_NAME, RUNTIME_MODULE_CACHE_VALUE_PROP_NAME, } from "~/utils/constants"; import { getReferencedVariablesInside } from "~/utils/get-referenced-variables-inside"; -import { isHookCall } from "~/utils/is-hook-call"; import { UnwrappedAssignmentEntry } from "~/utils/unwrap-pattern-assignment"; import { getDeclaredIdentifiersInLVal } from "../utils/get-declared-identifiers-in-lval"; import { Component } from "./Component"; +import { ComponentMutableSegment } from "./ComponentMutableSegment"; +import { findMutatingExpression } from "~/utils/find-mutating-expression"; -export class ComponentVariable { - private appliedModification = false; - - private computedDependencyGraph = false; - - // The side effects of this variable - private sideEffects = new Set>(); - - // ComponentVariables that reference this - private dependents = new Map(); - - // ComponentVariables that this depends on - private dependencies = new Map(); +export class ComponentVariable extends ComponentMutableSegment { + private runnableSegmentsMutatingThis = new Set(); constructor( + component: Component, + parent: ComponentMutableSegment | null = null, public binding: Binding, - public component: Component, private index: number - ) {} + ) { + super(component, parent, "ComponentVariable"); + } get name() { return this.binding.identifier.name; @@ -44,18 +38,10 @@ export class ComponentVariable { return this.index; } - addSideEffect(path: babel.NodePath) { - this.sideEffects.add(path); - } - - addDependency(componentVariable: ComponentVariable) { - this.dependencies.set(componentVariable.name, componentVariable); - } - unwrapAssignmentPatterns() { const { path, scope } = this.binding; - let newPaths: babel.NodePath[] = []; + const newPaths: babel.NodePath[] = []; let unwrappedEntries: UnwrappedAssignmentEntry[] = []; let unwrapVariableId: t.Identifier | null = null; @@ -64,7 +50,7 @@ export class ComponentVariable { if (path.isVariableDeclarator()) { const id = path.get("id"); const parentPath = - this.getParentStatement() as babel.NodePath; + this.path.getStatementParent() as babel.NodePath; const kind = parentPath.node.kind; if (id.isIdentifier()) { @@ -91,20 +77,26 @@ export class ComponentVariable { tempVariableDeclarator, ]); - const allNewPaths = parentPath.replaceWithMultiple([ - initDeclaration, - ...unwrapResult.unwrappedDeclarations, - ]); + [initPath] = parentPath.replaceWith(initDeclaration); + + unwrapResult.unwrappedDeclarations.forEach(([newNode, binding]) => { + if (!initPath) { + return; + } - initPath = allNewPaths[0]; - newPaths = allNewPaths.slice(1); + const [newPath] = initPath.insertAfter(newNode); + newPaths.push(newPath); + if (binding) { + binding.kind = newNode.kind as "const" | "let" | "var"; + binding.path = newPath; + } + }); scope.registerDeclaration(initPath); unwrappedEntries = unwrapResult.unwrappedEntries; } - // TODO: Check if the param is positioned in the first position if (this.binding.kind === "param") { const componentBlock = this.component.getFunctionBlockStatement(); @@ -112,7 +104,9 @@ export class ComponentVariable { return; } - unwrapVariableId = scope.generateUidIdentifier("props"); + unwrapVariableId = scope.generateUidIdentifier( + DEFAULT_UNWRAPPED_PROPS_VARIABLE_NAME + ); const unwrapResult = makeUnwrappedDeclarations( path as babel.NodePath, @@ -124,10 +118,14 @@ export class ComponentVariable { scope.registerBinding("param", initPath); - newPaths = componentBlock.unshiftContainer( - "body", - unwrapResult.unwrappedDeclarations - ); + unwrapResult.unwrappedDeclarations.forEach(([newNode, binding]) => { + const [newPath] = componentBlock.unshiftContainer("body", newNode); + newPaths.push(newPath); + if (binding) { + binding.kind = newNode.kind as "const" | "let" | "var"; + binding.path = newPath; + } + }); this.binding.kind = "let"; @@ -146,185 +144,176 @@ export class ComponentVariable { if (initPath) { // TODO: Refactor this const initId = initPath.isVariableDeclaration() - ? (initPath.get("declarations.0.id") as any) + ? (initPath.get( + "declarations.0.id" + ) as babel.NodePath) : initPath.isIdentifier() ? initPath : null; const initName = initId?.node.name; - if (initId) { - // TODO: Refactor this - scope - .getBinding(initName) - ?.reference( - newPath.isVariableDeclaration() - ? (newPath.get("declarations.0.id") as any) - : newPath - ); + if (initName) { + const newRefId = newPath.isVariableDeclaration() + ? (newPath.get( + "declarations.0.id" + ) as babel.NodePath) + : newPath; + + scope.getBinding(initName)?.reference(newRefId); } } } }); } - updateBinding(binding: Binding) { - this.binding = binding; + getDependencies() { + const allDependencies = new Set(super.getDependencies()); + + this.runnableSegmentsMutatingThis.forEach((runnableSegment) => { + runnableSegment.getDependencies().forEach((dependency) => { + allDependencies.add(dependency); + }); + }); - if (this.computedDependencyGraph) { - this.computeDependencyGraph(); - } + return allDependencies; } computeDependencyGraph() { this.dependencies.clear(); - this.dependents.clear(); - this.sideEffects.clear(); - - const visitDependencies = (dependencyIds: string[]) => { - dependencyIds.forEach((id) => { - let dependent = this.component.getComponentVariable(id); - - if (!dependent) { - const binding = this.component.path.scope.getBinding(id); - - if (binding) { - const newComponentVariable = - this.component.addComponentVariable(binding); - if (newComponentVariable) { - dependent = newComponentVariable; - } - } - } - if (dependent) { - this.dependents.set(id, dependent); - dependent.addDependency(this); + this.binding.referencePaths.forEach((mainReferencePath) => { + const statementParent = mainReferencePath.getStatementParent(); + const referencePath = mainReferencePath.isJSXExpressionContainer() + ? mainReferencePath.get("expression") + : mainReferencePath; + + let accessorPath = + referencePath.find( + (path) => + (path.isMemberExpression() || path.isOptionalMemberExpression()) && + path.isDescendant(statementParent!) + ) ?? referencePath; + + if ( + accessorPath.isOptionalMemberExpression() || + accessorPath.isMemberExpression() + ) { + if (accessorPath.parentPath.isCallExpression()) { + accessorPath = accessorPath.get( + "object" + ) as babel.NodePath; } - }); - }; + } - this.binding.referencePaths.forEach((referencePath) => { - const parentVariableDeclarator = referencePath.findParent( - (p) => - p.isVariableDeclarator() && this.component.isTheFunctionParentOf(p) - ) as babel.NodePath | null; + const accessorNode = accessorPath.node as t.Expression; + + if (statementParent?.isVariableDeclaration()) { + const parentVariableDeclarator = referencePath.find((p) => + p.isVariableDeclarator() + ) as babel.NodePath; - if (parentVariableDeclarator) { const lval = parentVariableDeclarator.get("id"); - const ids = getDeclaredIdentifiersInLVal(lval); + const dependentIds = getDeclaredIdentifiersInLVal(lval); + + dependentIds.forEach((id) => { + const binding = this.path.scope.getBinding(id); + if (!binding) { + return; + } - visitDependencies(ids); - } else if (referencePath.isIdentifier()) { - const referenceParent = referencePath.parentPath; + const dependent = this.component.addComponentVariable(binding); - // Handle function parameters - if ( - referenceParent.isFunction() && - referenceParent.get("params").some((param) => param === referencePath) - ) { - visitDependencies([referencePath.node.name]); + if (dependent) { + dependent.addDependency(this, accessorNode); + } + }); + return; + } + + // TODO: Add assignment pattern support here + + if (statementParent && !statementParent?.isReturnStatement()) { + const dependent = this.component.addRunnableSegment(statementParent); + dependent.addDependency(this, accessorNode); + + const mutatingExpressions = findMutatingExpression( + referencePath, + this.name + ); + if (mutatingExpressions) { + this.runnableSegmentsMutatingThis.add(dependent); } - } else { - // TODO: Side effect calculation } }); - const referencedVariablesInDeclaration = getReferencedVariablesInside( - this.binding.path + const referencedVariablesInDeclaration = Array.from( + getReferencedVariablesInside(this.binding.path).values() ); referencedVariablesInDeclaration.forEach((binding) => { this.component.addComponentVariable(binding); }); - - this.computedDependencyGraph = true; } - isDerived() { - return this.dependencies.size > 0; + getCacheUpdateEnqueueStatement() { + return makeCacheEnqueueCallStatement( + this.getCacheAccessorExpression(), + this.name + ); } - isHook() { - if (this.isDerived()) { - return false; + applyTransformation() { + if (this.appliedTransformation) { + return null; } - const path = this.binding.path; - - const parentVariableDeclarator = path.find( - ( - p: babel.NodePath - ): p is babel.NodePath => - p.isVariableDeclarator() && this.component.isTheFunctionParentOf(p) - ) as babel.NodePath | null; + const cacheUpdateEnqueueStatement = this.getCacheUpdateEnqueueStatement(); - if (!parentVariableDeclarator) { - return false; - } + switch (this.binding.kind) { + case "const": + case "let": + case "var": { + const hasHookCall = this.hasHookCall(); + const variableDeclaration = this.path.find((p) => + p.isVariableDeclaration() + ) as babel.NodePath; - const init = parentVariableDeclarator.get("init"); + const cacheValueAccessExpression = this.getCacheValueAccessExpression(); - if (!init.isCallExpression()) { - return false; - } + const dependencyConditions = this.makeDependencyCondition(); - return isHookCall(init); - } + const { prformTransformation, segmentCallableId, replacements } = + convertStatementToSegmentCallable(variableDeclaration, { + initialValue: cacheValueAccessExpression, + segmentCallableId: this.getSegmentCallableId(), + }); - applyModification() { - if (this.appliedModification) { - return; - } + this.appliedTransformation = true; - const cacheUpdateEnqueueStatement = makeCacheEnqueueCallStatement( - this.getCacheAccessorExpression(), - this.name - ); - - const valueDeclarationWithCache = t.variableDeclaration("let", [ - t.variableDeclarator( - t.identifier(this.name), - this.getCacheValueAccessExpression() - ), - ]); - - if ( - this.binding.kind === "const" || - this.binding.kind === "let" || - this.binding.kind === "var" - ) { - const variableDeclaration = - this.getParentStatement() as babel.NodePath; - - if (this.isHook()) { - variableDeclaration.insertAfter(cacheUpdateEnqueueStatement); - return; + return { + replacements, + segmentCallableId, + dependencyConditions, + prformTransformation, + hasHookCall, + updateCache: cacheUpdateEnqueueStatement, + }; } - const dependencyConditions = this.makeDependencyCondition(); - variableDeclaration.insertBefore(valueDeclarationWithCache); - variableDeclaration.replaceWith( - t.ifStatement( - dependencyConditions, - t.blockStatement([ - ...variableDeclarationToAssignment(variableDeclaration), - cacheUpdateEnqueueStatement, - ]) - ) - ); - } else if (this.binding.kind === "param") { - this.component - .getFunctionBlockStatement() - ?.unshiftContainer("body", cacheUpdateEnqueueStatement); + case "param": { + this.component + .getFunctionBlockStatement() + ?.unshiftContainer("body", cacheUpdateEnqueueStatement); + this.appliedTransformation = true; + break; + } } - this.appliedModification = true; - } + this.appliedTransformation = true; - private makeDependencyCondition() { - return makeDependencyCondition(this); + return null; } private getCacheAccessorExpression() { @@ -349,20 +338,7 @@ export class ComponentVariable { ); } - getDependencies() { - return this.dependencies; - } - - private getParentStatement() { - return this.binding.path.getStatementParent(); - } - - // --- DEBUGGING --- - __debug_getDependents() { - return this.dependents; - } - - __debug_getSideEffects() { - return this.sideEffects; + get path() { + return this.binding.path; } } diff --git a/packages/compiler/src/classes/tests/Component-basic.test.ts b/packages/compiler/src/classes/tests/Component-basic.test.ts index e9616e4..56ea36a 100644 --- a/packages/compiler/src/classes/tests/Component-basic.test.ts +++ b/packages/compiler/src/classes/tests/Component-basic.test.ts @@ -14,7 +14,7 @@ describe.skip("ComponentVariable", () => { describe("computeDependencyGraph", () => { it("basic example", () => { const [component] = parseCodeAndRun("fixture_1"); - component.computeComponentVariables(); + component.computeComponentSegments(); const componentVariables = component.__debug_getComponentVariables(); @@ -26,37 +26,25 @@ describe.skip("ComponentVariable", () => { "setState", ]); - // state has myDerivedVariable as dependent - expect([ - ...componentVariables.get("state")!.__debug_getDependents().keys(), - ]).toStrictEqual(["myDerivedVariable"]); + // // state depends on nothing + // expect([ + // ...componentVariables.get("state")!.getDependencies().keys(), + // ]).toStrictEqual(["_unwrapped"]); - // state depends on nothing - expect([ - ...componentVariables.get("state")!.getDependencies().keys(), - ]).toStrictEqual(["_unwrapped"]); + // // myDerivedVariable depends on state + // expect([ + // ...componentVariables + // .get("myDerivedVariable")! + // .getDependencies() + // .keys(), + // ]).toStrictEqual(["state"]); - // myDerivedVariable depends on state - expect([ - ...componentVariables - .get("myDerivedVariable")! - .getDependencies() - .keys(), - ]).toStrictEqual(["state"]); - - // myDerivedVariable has no dependents - expect([ - ...componentVariables - .get("myDerivedVariable")! - .__debug_getDependents() - .keys(), - ]).toStrictEqual([]); }); describe("getRootComponentVariables", () => { it("returns all root component variables", () => { const [component] = parseCodeAndRun("fixture_2"); - component.computeComponentVariables(); + component.computeComponentSegments(); const rootComponentVariables = component.getRootComponentVariables(); diff --git a/packages/compiler/src/classes/tests/Component-fixtures.test.ts b/packages/compiler/src/classes/tests/Component-fixtures.test.ts index 9e95b5d..81dce3b 100644 --- a/packages/compiler/src/classes/tests/Component-fixtures.test.ts +++ b/packages/compiler/src/classes/tests/Component-fixtures.test.ts @@ -3,19 +3,22 @@ import { parseFixture } from "~/utils/testing"; const parseCodeAndRun = (fixtureName: string) => { const programPath = parseFixture(fixtureName); - const [component] = findComponents(programPath); + const components = findComponents(programPath); - return [component!, programPath] as const; + components.forEach((component) => { + component.computeComponentSegments(); + component.applyTransformation(); + }); + + return [components, programPath] as const; }; describe("Component fixtures", () => { - describe.only("applyModification", () => { - it.each(["fixture_1", "fixture_2", "fixture_4", "fixture_3"])( + describe("applyModification", () => { + it.each(Array.from({ length: 12 }, (_, i) => `fixture_${i + 1}`))( "%s", (fixtureName) => { - const [component, program] = parseCodeAndRun(fixtureName); - component.computeComponentVariables(); - component.applyModification(); + const [, program] = parseCodeAndRun(fixtureName); const codeAfter = program.toString(); diff --git a/packages/compiler/src/classes/tests/__snapshots__/Component-fixtures.test.ts.snap b/packages/compiler/src/classes/tests/__snapshots__/Component-fixtures.test.ts.snap index 7b666af..fb65f98 100644 --- a/packages/compiler/src/classes/tests/__snapshots__/Component-fixtures.test.ts.snap +++ b/packages/compiler/src/classes/tests/__snapshots__/Component-fixtures.test.ts.snap @@ -2,46 +2,74 @@ exports[`Component fixtures applyModification fixture_1 1`] = ` "export function MyComponent() { - const [_$unforgetCache, _$unforgetCommit] = useCreateCache$unforget( + const [_$unforgetCache, _$unforgetCommit, _$unforgetNull] = useCreateCache$unforget( /* 0 => _unwrappedJsxEl - 1 => myDerivedVariable - 2 => state - 3 => _unwrapped - 4 => setState + 1 => _unwrappedJsxExp + 2 => value */ - 5); - const _unwrapped = useState(0); - _$unforgetCache[3].e(_unwrapped); - let state = _$unforgetCache[2].v; - if (_$unforgetCache[2].n || _unwrapped[0] !== _$unforgetCache[3].v[0]) { - state = _unwrapped[0]; - _$unforgetCache[2].e(state); + 3); + let i; + const _segment = () => { + i = 2; + }; + let k; + const _segment2 = () => { + k = "hi"; + }; + let m; + const _segment3 = () => { + m = i; + }; + const _segment4 = () => { + if (m === 2) { + return null; + } + }; + let value = _$unforgetCache[2].v; + const _segment5 = () => { + value = []; + }; + const _segment6 = () => { + value.push(...["Hello", "World", i]); + }; + let _unwrappedJsxExp = _$unforgetCache[1].v; + const _segment7 = () => { + _unwrappedJsxExp = value[0]; + }; + let _unwrappedJsxEl = _$unforgetCache[0].v; + const _segment8 = () => { + _unwrappedJsxEl =
{_unwrappedJsxExp}
; + }; + _segment(); + _segment2(); + _segment3(); + const _temp = _segment4(); + if (_temp !== _$unforgetNull) { + return _temp; } - let setState = _$unforgetCache[4].v; - if (_$unforgetCache[4].n || _unwrapped[1] !== _$unforgetCache[3].v[1]) { - setState = _unwrapped[1]; - _$unforgetCache[4].e(setState); + if (_$unforgetCache[2].n || value !== _$unforgetCache[2].v) { + _segment5(); + _$unforgetCache[2].e(value); } - let myDerivedVariable = _$unforgetCache[1].v; - if (_$unforgetCache[1].n || state !== _$unforgetCache[2].v) { - myDerivedVariable = state + 1; - _$unforgetCache[1].e(myDerivedVariable); + if (value !== _$unforgetCache[2].v) { + _segment6(); } - const unusedVariable = 1; - let _unwrappedJsxEl = _$unforgetCache[0].v; - if (_$unforgetCache[0].n || myDerivedVariable !== _$unforgetCache[1].v) { - _unwrappedJsxEl =
{myDerivedVariable}
; + if (_$unforgetCache[1].n || value[0] !== _$unforgetCache[2].v[0]) { + _segment7(); + _$unforgetCache[1].e(_unwrappedJsxExp); + } + if (_$unforgetCache[0].n || _unwrappedJsxExp !== _$unforgetCache[1].v) { + _segment8(); _$unforgetCache[0].e(_unwrappedJsxEl); } - _$unforgetCommit(); return _unwrappedJsxEl; }" `; exports[`Component fixtures applyModification fixture_2 1`] = ` "function MyComponent(_props) { - const [_$unforgetCache, _$unforgetCommit] = useCreateCache$unforget( + const [_$unforgetCache, _$unforgetCommit, _$unforgetNull] = useCreateCache$unforget( /* 0 => _unwrappedJsxEl 1 => myDerivedVariable @@ -52,38 +80,59 @@ exports[`Component fixtures applyModification fixture_2 1`] = ` 6 => _props */ 7); - _$unforgetCache[6].e(_props); + let _unwrapped = _$unforgetCache[3].v; + const _segment = () => { + _unwrapped = useState(0); + }; + let unusedVariable; + const _segment2 = () => { + unusedVariable = 1; + }; let someProp = _$unforgetCache[5].v; - if (_$unforgetCache[5].n || _props.someProp !== _$unforgetCache[6].v.someProp) { + const _segment3 = () => { someProp = _props.someProp; - _$unforgetCache[5].e(someProp); - } - const _unwrapped = useState(0); - _$unforgetCache[3].e(_unwrapped); + }; + let setState = _$unforgetCache[4].v; + const _segment4 = () => { + setState = _unwrapped[1]; + }; let state = _$unforgetCache[2].v; - if (_$unforgetCache[2].n || _unwrapped[0] !== _$unforgetCache[3].v[0]) { + const _segment5 = () => { state = _unwrapped[0]; - _$unforgetCache[2].e(state); + }; + let myDerivedVariable = _$unforgetCache[1].v; + const _segment6 = () => { + myDerivedVariable = state + 1; + }; + let _unwrappedJsxEl = _$unforgetCache[0].v; + const _segment7 = () => { + _unwrappedJsxEl =
+ {myDerivedVariable} {someProp} +
; + }; + _segment(); + _$unforgetCache[3].e(_unwrapped); + _segment2(); + if (_$unforgetCache[5].n || _props !== _$unforgetCache[6].v) { + _segment3(); + _$unforgetCache[5].e(someProp); } - let setState = _$unforgetCache[4].v; - if (_$unforgetCache[4].n || _unwrapped[1] !== _$unforgetCache[3].v[1]) { - setState = _unwrapped[1]; + if (_$unforgetCache[4].n || _unwrapped !== _$unforgetCache[3].v) { + _segment4(); _$unforgetCache[4].e(setState); } - let myDerivedVariable = _$unforgetCache[1].v; + if (_$unforgetCache[2].n || _unwrapped !== _$unforgetCache[3].v) { + _segment5(); + _$unforgetCache[2].e(state); + } if (_$unforgetCache[1].n || state !== _$unforgetCache[2].v) { - myDerivedVariable = state + 1; + _segment6(); _$unforgetCache[1].e(myDerivedVariable); } - const unusedVariable = 1; - let _unwrappedJsxEl = _$unforgetCache[0].v; if (_$unforgetCache[0].n || myDerivedVariable !== _$unforgetCache[1].v || someProp !== _$unforgetCache[5].v) { - _unwrappedJsxEl =
- {myDerivedVariable} {someProp} -
; + _segment7(); _$unforgetCache[0].e(_unwrappedJsxEl); } - _$unforgetCommit(); return _unwrappedJsxEl; } export default MyComponent;" @@ -93,7 +142,7 @@ exports[`Component fixtures applyModification fixture_3 1`] = ` "const varDefinedOutside = 1; const generateValue = () => 2; function MyComponent(_props) { - const [_$unforgetCache, _$unforgetCommit] = useCreateCache$unforget( + const [_$unforgetCache, _$unforgetCommit, _$unforgetNull] = useCreateCache$unforget( /* 0 => _unwrappedJsxEl 1 => handleIncrement @@ -108,62 +157,95 @@ function MyComponent(_props) { 10 => someGeneratedValue */ 11); - _$unforgetCache[8].e(_props); - let someProp = _$unforgetCache[7].v; - if (_$unforgetCache[7].n || _props.someProp !== _$unforgetCache[8].v.someProp) { - someProp = _props.someProp; - _$unforgetCache[7].e(someProp); - } - const _unwrapped = useState(0); - _$unforgetCache[3].e(_unwrapped); - let state = _$unforgetCache[4].v; - if (_$unforgetCache[4].n || _unwrapped[0] !== _$unforgetCache[3].v[0]) { - state = _unwrapped[0]; - _$unforgetCache[4].e(state); - } - let setState = _$unforgetCache[2].v; - if (_$unforgetCache[2].n || _unwrapped[1] !== _$unforgetCache[3].v[1]) { - setState = _unwrapped[1]; - _$unforgetCache[2].e(setState); - } + let _unwrapped = _$unforgetCache[3].v; + const _segment = () => { + _unwrapped = useState(0); + }; let handleIncrement = _$unforgetCache[1].v; - if (_$unforgetCache[1].n || setState !== _$unforgetCache[2].v || state !== _$unforgetCache[4].v) { + const _segment2 = () => { handleIncrement = () => { setState(state + 1); console.log("current state", state); }; - _$unforgetCache[1].e(handleIncrement); - } + }; + let someGeneratedValue = _$unforgetCache[10].v; + const _segment3 = () => { + someGeneratedValue = generateValue(); + }; + let unusedVariable; + const _segment4 = () => { + unusedVariable = 1; + }; + let valueDerivedFromDefinedOutside = _$unforgetCache[9].v; + const _segment5 = () => { + valueDerivedFromDefinedOutside = varDefinedOutside * 10; + }; + let someProp = _$unforgetCache[7].v; + const _segment6 = () => { + someProp = _props.someProp; + }; + let setState = _$unforgetCache[2].v; + const _segment7 = () => { + setState = _unwrapped[1]; + }; + let state = _$unforgetCache[4].v; + const _segment8 = () => { + state = _unwrapped[0]; + }; let myDerivedVariable = _$unforgetCache[5].v; - if (_$unforgetCache[5].n || state !== _$unforgetCache[4].v) { + const _segment9 = () => { myDerivedVariable = state + 1; - _$unforgetCache[5].e(myDerivedVariable); - } + }; let propDerivedVariable = _$unforgetCache[6].v; - if (_$unforgetCache[6].n || someProp !== _$unforgetCache[7].v) { + const _segment10 = () => { propDerivedVariable = someProp + "_" + someProp; - _$unforgetCache[6].e(propDerivedVariable); + }; + let _unwrappedJsxEl = _$unforgetCache[0].v; + const _segment11 = () => { + _unwrappedJsxEl = ; + }; + _segment(); + _$unforgetCache[3].e(_unwrapped); + if (_$unforgetCache[1].n) { + _segment2(); + _$unforgetCache[1].e(handleIncrement); } - let someGeneratedValue = _$unforgetCache[10].v; if (_$unforgetCache[10].n) { - someGeneratedValue = generateValue(); + _segment3(); _$unforgetCache[10].e(someGeneratedValue); } - const unusedVariable = 1; - let valueDerivedFromDefinedOutside = _$unforgetCache[9].v; + _segment4(); if (_$unforgetCache[9].n) { - valueDerivedFromDefinedOutside = varDefinedOutside * 10; + _segment5(); _$unforgetCache[9].e(valueDerivedFromDefinedOutside); } - let _unwrappedJsxEl = _$unforgetCache[0].v; + if (_$unforgetCache[7].n || _props !== _$unforgetCache[8].v) { + _segment6(); + _$unforgetCache[7].e(someProp); + } + if (_$unforgetCache[2].n || _unwrapped !== _$unforgetCache[3].v) { + _segment7(); + _$unforgetCache[2].e(setState); + } + if (_$unforgetCache[4].n || _unwrapped !== _$unforgetCache[3].v) { + _segment8(); + _$unforgetCache[4].e(state); + } + if (_$unforgetCache[5].n || state !== _$unforgetCache[4].v) { + _segment9(); + _$unforgetCache[5].e(myDerivedVariable); + } + if (_$unforgetCache[6].n || someProp !== _$unforgetCache[7].v) { + _segment10(); + _$unforgetCache[6].e(propDerivedVariable); + } if (_$unforgetCache[0].n || handleIncrement !== _$unforgetCache[1].v || myDerivedVariable !== _$unforgetCache[5].v || propDerivedVariable !== _$unforgetCache[6].v || valueDerivedFromDefinedOutside !== _$unforgetCache[9].v || someGeneratedValue !== _$unforgetCache[10].v) { - _unwrappedJsxEl = ; + _segment11(); _$unforgetCache[0].e(_unwrappedJsxEl); } - _$unforgetCommit(); return _unwrappedJsxEl; } export { MyComponent };" @@ -174,89 +256,1046 @@ exports[`Component fixtures applyModification fixture_4 1`] = ` return n * 2; } export function MyComponent(_props) { - const [_$unforgetCache, _$unforgetCommit] = useCreateCache$unforget( + const [_$unforgetCache, _$unforgetCommit, _$unforgetNull] = useCreateCache$unforget( /* 0 => _unwrappedJsxEl5 1 => count 2 => doubleCount 3 => _unwrappedJsxExp - 4 => _unwrappedJsxEl3 - 5 => setCount - 6 => _unwrappedJsxExp2 - 7 => _unwrappedJsxEl4 - 8 => _unwrapped - 9 => _unwrappedJsxEl - 10 => someProp - 11 => _props - 12 => _unwrappedJsxEl2 + 4 => setCount + 5 => _unwrappedJsxExp2 + 6 => _unwrapped + 7 => _unwrappedJsxEl + 8 => someProp + 9 => _props + 10 => _unwrappedJsxEl2 + 11 => _unwrappedJsxEl3 + 12 => _unwrappedJsxEl4 */ 13); - _$unforgetCache[11].e(_props); - let someProp = _$unforgetCache[10].v; - if (_$unforgetCache[10].n || _props.someProp !== _$unforgetCache[11].v.someProp) { + let _unwrapped = _$unforgetCache[6].v; + const _segment = () => { + _unwrapped = useState(0); + }; + let _unwrappedJsxEl = _$unforgetCache[7].v; + const _segment2 = () => { + _unwrappedJsxEl =
; + }; + let _unwrappedJsxEl2 = _$unforgetCache[10].v; + const _segment3 = () => { + _unwrappedJsxEl2 =
; + }; + let _unwrappedJsxEl3 = _$unforgetCache[11].v; + const _segment4 = () => { + _unwrappedJsxEl3 = ; + }; + let _unwrappedJsxEl4 = _$unforgetCache[12].v; + const _segment5 = () => { + _unwrappedJsxEl4 = ; + }; + let someProp = _$unforgetCache[8].v; + const _segment6 = () => { someProp = _props.someProp; - _$unforgetCache[10].e(someProp); - } - const _unwrapped = useState(0); - _$unforgetCache[8].e(_unwrapped); + }; + let setCount = _$unforgetCache[4].v; + const _segment7 = () => { + setCount = _unwrapped[1]; + }; let count = _$unforgetCache[1].v; - if (_$unforgetCache[1].n || _unwrapped[0] !== _$unforgetCache[8].v[0]) { + const _segment8 = () => { count = _unwrapped[0]; - _$unforgetCache[1].e(count); + }; + let doubleCount = _$unforgetCache[2].v; + const _segment9 = () => { + doubleCount = double(count); + }; + let _unwrappedJsxExp = _$unforgetCache[3].v; + const _segment10 = () => { + _unwrappedJsxExp = () => setCount(count + 1); + }; + let _unwrappedJsxExp2 = _$unforgetCache[5].v; + const _segment11 = () => { + _unwrappedJsxExp2 = () => setCount(count - 1); + }; + let _unwrappedJsxEl5 = _$unforgetCache[0].v; + const _segment12 = () => { + _unwrappedJsxEl5 =
+ Hello! Current count is {count} and its double is {doubleCount} + {_unwrappedJsxEl} + The prop is {someProp} + {_unwrappedJsxEl2} + {_unwrappedJsxEl3} + {_unwrappedJsxEl4} +
; + }; + _segment(); + _$unforgetCache[6].e(_unwrapped); + if (_$unforgetCache[7].n) { + _segment2(); + _$unforgetCache[7].e(_unwrappedJsxEl); } - let setCount = _$unforgetCache[5].v; - if (_$unforgetCache[5].n || _unwrapped[1] !== _$unforgetCache[8].v[1]) { - setCount = _unwrapped[1]; - _$unforgetCache[5].e(setCount); + if (_$unforgetCache[10].n) { + _segment3(); + _$unforgetCache[10].e(_unwrappedJsxEl2); + } + if (_$unforgetCache[11].n) { + _segment4(); + _$unforgetCache[11].e(_unwrappedJsxEl3); + } + if (_$unforgetCache[12].n) { + _segment5(); + _$unforgetCache[12].e(_unwrappedJsxEl4); + } + if (_$unforgetCache[8].n || _props !== _$unforgetCache[9].v) { + _segment6(); + _$unforgetCache[8].e(someProp); + } + if (_$unforgetCache[4].n || _unwrapped !== _$unforgetCache[6].v) { + _segment7(); + _$unforgetCache[4].e(setCount); + } + if (_$unforgetCache[1].n || _unwrapped !== _$unforgetCache[6].v) { + _segment8(); + _$unforgetCache[1].e(count); } - let doubleCount = _$unforgetCache[2].v; if (_$unforgetCache[2].n || count !== _$unforgetCache[1].v) { - doubleCount = double(count); + _segment9(); _$unforgetCache[2].e(doubleCount); } - let _unwrappedJsxExp = _$unforgetCache[3].v; - if (_$unforgetCache[3].n || setCount !== _$unforgetCache[5].v || count !== _$unforgetCache[1].v) { - _unwrappedJsxExp = () => setCount(count + 1); + if (_$unforgetCache[3].n || setCount !== _$unforgetCache[4].v || count !== _$unforgetCache[1].v) { + _segment10(); _$unforgetCache[3].e(_unwrappedJsxExp); } - let _unwrappedJsxExp2 = _$unforgetCache[6].v; - if (_$unforgetCache[6].n || setCount !== _$unforgetCache[5].v || count !== _$unforgetCache[1].v) { - _unwrappedJsxExp2 = () => setCount(count - 1); - _$unforgetCache[6].e(_unwrappedJsxExp2); + if (_$unforgetCache[5].n || setCount !== _$unforgetCache[4].v || count !== _$unforgetCache[1].v) { + _segment11(); + _$unforgetCache[5].e(_unwrappedJsxExp2); } - let _unwrappedJsxEl = _$unforgetCache[9].v; - if (_$unforgetCache[9].n) { - _unwrappedJsxEl =
; - _$unforgetCache[9].e(_unwrappedJsxEl); + if (_$unforgetCache[0].n || doubleCount !== _$unforgetCache[2].v || count !== _$unforgetCache[1].v || _unwrappedJsxExp !== _$unforgetCache[3].v || _unwrappedJsxExp2 !== _$unforgetCache[5].v || _unwrappedJsxEl !== _$unforgetCache[7].v || someProp !== _$unforgetCache[8].v || _unwrappedJsxEl2 !== _$unforgetCache[10].v || _unwrappedJsxEl3 !== _$unforgetCache[11].v || _unwrappedJsxEl4 !== _$unforgetCache[12].v) { + _segment12(); + _$unforgetCache[0].e(_unwrappedJsxEl5); } - let _unwrappedJsxEl2 = _$unforgetCache[12].v; - if (_$unforgetCache[12].n) { - _unwrappedJsxEl2 =
; - _$unforgetCache[12].e(_unwrappedJsxEl2); + return _unwrappedJsxEl5; +}" +`; + +exports[`Component fixtures applyModification fixture_5 1`] = ` +"const words = ["Shawshank", "Redemption", "Godfather", "Dark", "Knight", "Forest", "Gump", "Private", "Ryan"]; +const initalMovies = [{ + title: "The Shawshank Redemption", + year: 1994 +}, { + title: "The Godfather", + year: 1972 +}, { + title: "The Dark Knight", + year: 2008 +}]; +const useMovies = () => { + const [movies, setMovies] = useState(initalMovies); + const addRandomMovie = () => { + setMovies(p => [...p, { + title: "The " + words[Math.floor(Math.random() * words.length)] + " " + words[Math.floor(Math.random() * words.length)], + year: 1990 + Math.floor(Math.random() * 20) + }]); + }; + return [movies, addRandomMovie]; +}; +const useKey = () => "key1"; +const object = { + key1: "value1", + key2: "value2" +}; +export function MyComponent() { + const [_$unforgetCache, _$unforgetCommit, _$unforgetNull] = useCreateCache$unforget( + /* + 0 => _unwrappedJsxEl3 + 1 => _unwrappedJsxEl + 2 => addRandomMovie + 3 => _unwrapped + 4 => movies + 5 => _unwrappedJsxEl2 + 6 => _unwrappedJsxExp + 7 => _unwrappedJsxExp2 + 8 => filteredMovies + 9 => value + 10 => key + */ + 11); + let _unwrapped = _$unforgetCache[3].v; + const _segment = () => { + _unwrapped = useMovies(); + }; + let key = _$unforgetCache[10].v; + const _segment2 = () => { + key = useKey(); + }; + let i; + const _segment3 = () => { + i = 5; + }; + let _unwrappedJsxExp = _$unforgetCache[6].v; + const _segment4 = () => { + _unwrappedJsxExp = movies.length; + }; + let addRandomMovie = _$unforgetCache[2].v; + const _segment5 = () => { + addRandomMovie = _unwrapped[1]; + }; + let movies = _$unforgetCache[4].v; + const _segment6 = () => { + movies = _unwrapped[0]; + }; + let value = _$unforgetCache[9].v; + const _segment7 = () => { + value = object[key]; + }; + let filteredMovies = _$unforgetCache[8].v; + const _segment8 = () => { + filteredMovies = []; + }; + const _segment9 = () => { + for (let i = 0; i < movies.length; i++) { + const _segment20 = () => { + if (movies[i].year > 2000) { + const _segment21 = () => { + filteredMovies.push(movies[i]); + }; + const _unwrapped = useMovies(); + const movies = _unwrapped[0]; + const filteredMovies = []; + if (movies !== _$unforgetCache[4].v || filteredMovies !== _$unforgetCache[8].v) { + _segment21(); + } + } + }; + const _unwrapped = useMovies(); + const movies = _unwrapped[0]; + if (movies[i] !== _$unforgetCache[4].v[i]) { + _segment20(); + } + } + }; + const _segment10 = () => { + if (filteredMovies.length > 0) { + // Just some side effect + const _segment22 = () => { + console.log("Movies after 2000: ", filteredMovies); + }; + const filteredMovies = []; + if (filteredMovies !== _$unforgetCache[8].v) { + _segment22(); + } + } + }; + const _segment11 = () => { + console.log("Total number of movies: ", movies.length); + }; + let _unwrappedJsxExp2 = _$unforgetCache[7].v; + const _segment12 = () => { + _unwrappedJsxExp2 = filteredMovies.map(movie =>
+ {movie.title} + {value} +
); + }; + let _unwrappedJsxEl = _$unforgetCache[1].v; + const _segment13 = () => { + _unwrappedJsxEl = ; + }; + let _unwrappedJsxEl2 = _$unforgetCache[5].v; + const _segment14 = () => { + _unwrappedJsxEl2 =
total number of movies: {_unwrappedJsxExp}
; + }; + let _unwrappedJsxEl3 = _$unforgetCache[0].v; + const _segment15 = () => { + _unwrappedJsxEl3 =
+ {_unwrappedJsxEl} + {_unwrappedJsxEl2} + {_unwrappedJsxExp2} +
; + }; + _segment(); + _$unforgetCache[3].e(_unwrapped); + _segment2(); + _$unforgetCache[10].e(key); + _segment3(); + if (_$unforgetCache[6].n) { + _segment4(); + _$unforgetCache[6].e(_unwrappedJsxExp); } - let _unwrappedJsxEl3 = _$unforgetCache[4].v; - if (_$unforgetCache[4].n || _unwrappedJsxExp !== _$unforgetCache[3].v) { - _unwrappedJsxEl3 = ; - _$unforgetCache[4].e(_unwrappedJsxEl3); + if (_$unforgetCache[2].n || _unwrapped !== _$unforgetCache[3].v) { + _segment5(); + _$unforgetCache[2].e(addRandomMovie); } - let _unwrappedJsxEl4 = _$unforgetCache[7].v; - if (_$unforgetCache[7].n || _unwrappedJsxExp2 !== _$unforgetCache[6].v) { - _unwrappedJsxEl4 = ; - _$unforgetCache[7].e(_unwrappedJsxEl4); + if (_$unforgetCache[4].n || _unwrapped !== _$unforgetCache[3].v) { + _segment6(); + _$unforgetCache[4].e(movies); } - let _unwrappedJsxEl5 = _$unforgetCache[0].v; - if (_$unforgetCache[0].n || count !== _$unforgetCache[1].v || doubleCount !== _$unforgetCache[2].v || _unwrappedJsxEl !== _$unforgetCache[9].v || someProp !== _$unforgetCache[10].v || _unwrappedJsxEl2 !== _$unforgetCache[12].v || _unwrappedJsxEl3 !== _$unforgetCache[4].v || _unwrappedJsxEl4 !== _$unforgetCache[7].v) { - _unwrappedJsxEl5 =
- Hello! Current count is {count} and its double is {doubleCount} + if (_$unforgetCache[9].n || key[key] !== _$unforgetCache[10].v[key]) { + _segment7(); + _$unforgetCache[9].e(value); + } + if (_$unforgetCache[8].n || movies !== _$unforgetCache[4].v || filteredMovies !== _$unforgetCache[8].v) { + _segment8(); + _$unforgetCache[8].e(filteredMovies); + } + if (movies.length !== _$unforgetCache[4].v.length) { + _segment9(); + } + if (filteredMovies.length !== _$unforgetCache[8].v.length) { + _segment10(); + } + if (movies !== _$unforgetCache[4].v) { + _segment11(); + } + if (_$unforgetCache[7].n || filteredMovies !== _$unforgetCache[8].v || value !== _$unforgetCache[9].v) { + _segment12(); + _$unforgetCache[7].e(_unwrappedJsxExp2); + } + if (_$unforgetCache[1].n || addRandomMovie !== _$unforgetCache[2].v) { + _segment13(); + _$unforgetCache[1].e(_unwrappedJsxEl); + } + if (_$unforgetCache[5].n || _unwrappedJsxExp !== _$unforgetCache[6].v) { + _segment14(); + _$unforgetCache[5].e(_unwrappedJsxEl2); + } + if (_$unforgetCache[0].n || _unwrappedJsxEl !== _$unforgetCache[1].v || movies !== _$unforgetCache[4].v || _unwrappedJsxEl2 !== _$unforgetCache[5].v || _unwrappedJsxExp2 !== _$unforgetCache[7].v) { + _segment15(); + _$unforgetCache[0].e(_unwrappedJsxEl3); + } + return _unwrappedJsxEl3; +} +export default MyComponent;" +`; + +exports[`Component fixtures applyModification fixture_6 1`] = ` +"export function SimpleJSX() { + const [_$unforgetCache, _$unforgetCommit, _$unforgetNull] = useCreateCache$unforget( + /* + 0 => _unwrappedJsxEl + */ + 1); + let _unwrappedJsxEl = _$unforgetCache[0].v; + const _segment = () => { + _unwrappedJsxEl =
; + }; + if (_$unforgetCache[0].n) { + _segment(); + _$unforgetCache[0].e(_unwrappedJsxEl); + } + return _unwrappedJsxEl; +}" +`; + +exports[`Component fixtures applyModification fixture_7 1`] = ` +"export function SimpleJSX() { + const [_$unforgetCache, _$unforgetCommit, _$unforgetNull] = useCreateCache$unforget( + /* + 0 => _unwrappedJsxEl3 + 1 => _unwrappedJsxEl + 2 => _unwrappedJsxEl2 + 3 => value + */ + 4); + let value = _$unforgetCache[3].v; + const _segment = () => { + value = useValue(); + }; + let _unwrappedJsxEl = _$unforgetCache[1].v; + const _segment2 = () => { + _unwrappedJsxEl = ; + }; + let _unwrappedJsxEl2 = _$unforgetCache[2].v; + const _segment3 = () => { + _unwrappedJsxEl2 = {value}; + }; + let _unwrappedJsxEl3 = _$unforgetCache[0].v; + const _segment4 = () => { + _unwrappedJsxEl3 =
{_unwrappedJsxEl} - The prop is {someProp} + {_unwrappedJsxEl2} +
; + }; + _segment(); + _$unforgetCache[3].e(value); + if (_$unforgetCache[1].n) { + _segment2(); + _$unforgetCache[1].e(_unwrappedJsxEl); + } + if (_$unforgetCache[2].n || value !== _$unforgetCache[3].v) { + _segment3(); + _$unforgetCache[2].e(_unwrappedJsxEl2); + } + if (_$unforgetCache[0].n || _unwrappedJsxEl !== _$unforgetCache[1].v || _unwrappedJsxEl2 !== _$unforgetCache[2].v) { + _segment4(); + _$unforgetCache[0].e(_unwrappedJsxEl3); + } + return _unwrappedJsxEl3; +}" +`; + +exports[`Component fixtures applyModification fixture_8 1`] = ` +"export function SimpleJSX() { + const [_$unforgetCache, _$unforgetCommit, _$unforgetNull] = useCreateCache$unforget( + /* + 0 => _unwrappedJsxEl + 1 => _unwrappedJsxEl2 + */ + 2); + let value; + const _segment = () => { + value = useValue(); + }; + const _segment2 = () => { + if (value === "loading") { + let _unwrappedJsxEl = _$unforgetCache[0].v; + const _segment4 = () => { + _unwrappedJsxEl =
Loading...
; + }; + if (_$unforgetCache[0].n) { + _segment4(); + _$unforgetCache[0].e(_unwrappedJsxEl); + } + return _unwrappedJsxEl; + } + }; + let _unwrappedJsxEl2 = _$unforgetCache[1].v; + const _segment3 = () => { + _unwrappedJsxEl2 = ; + }; + _segment(); + const _temp = _segment2(); + if (_temp !== _$unforgetNull) { + return _temp; + } + if (_$unforgetCache[1].n) { + _segment3(); + _$unforgetCache[1].e(_unwrappedJsxEl2); + } + return _unwrappedJsxEl2; +}" +`; + +exports[`Component fixtures applyModification fixture_9 1`] = ` +"function useValue() { + const [value, setValue] = useState(Math.random()); + useEffect(() => { + const interval = setInterval(() => { + setValue(Math.random()); + }, 1000); + return () => clearInterval(interval); + }, []); + return value; +} +export function SimpleJSX() { + const [_$unforgetCache, _$unforgetCommit, _$unforgetNull] = useCreateCache$unforget( + /* + 0 => _unwrappedJsxEl + 1 => valueWith2Decimal + 2 => derivedValue + 3 => value + 4 => _unwrappedJsxEl5 + 5 => _unwrappedJsxEl2 + 6 => _unwrappedJsxEl3 + 7 => _unwrappedJsxEl4 + */ + 8); + let value = _$unforgetCache[3].v; + const _segment = () => { + value = useValue(); + }; + let _unwrappedJsxEl2 = _$unforgetCache[5].v; + const _segment2 = () => { + _unwrappedJsxEl2 = ; + }; + let _unwrappedJsxEl3 = _$unforgetCache[6].v; + const _segment3 = () => { + _unwrappedJsxEl3 =
{value}
; + }; + let _unwrappedJsxEl4 = _$unforgetCache[7].v; + const _segment4 = () => { + _unwrappedJsxEl4 =
{derivedValue}
; + }; + let valueWith2Decimal = _$unforgetCache[1].v; + const _segment5 = () => { + valueWith2Decimal = value.toFixed(2); + }; + const _segment6 = () => { + if (value > 0.8) { + let _unwrappedJsxEl = _$unforgetCache[0].v; + const _segment11 = () => { + _unwrappedJsxEl =
Loading because value is {valueWith2Decimal}...
; + }; + const value = useValue(); + const valueWith2Decimal = value.toFixed(2); + if (_$unforgetCache[0].n || valueWith2Decimal !== _$unforgetCache[1].v) { + _segment11(); + _$unforgetCache[0].e(_unwrappedJsxEl); + } + return _unwrappedJsxEl; + } + }; + let derivedValue = _$unforgetCache[2].v; + const _segment7 = () => { + derivedValue = "state updated to: " + valueWith2Decimal; + }; + let _unwrappedJsxEl5 = _$unforgetCache[4].v; + const _segment8 = () => { + _unwrappedJsxEl5 =
{_unwrappedJsxEl2} {_unwrappedJsxEl3} {_unwrappedJsxEl4}
; - _$unforgetCache[0].e(_unwrappedJsxEl5); + }; + _segment(); + _$unforgetCache[3].e(value); + if (_$unforgetCache[5].n) { + _segment2(); + _$unforgetCache[5].e(_unwrappedJsxEl2); + } + if (_$unforgetCache[6].n) { + _segment3(); + _$unforgetCache[6].e(_unwrappedJsxEl3); + } + if (_$unforgetCache[7].n) { + _segment4(); + _$unforgetCache[7].e(_unwrappedJsxEl4); + } + if (_$unforgetCache[1].n || value !== _$unforgetCache[3].v) { + _segment5(); + _$unforgetCache[1].e(valueWith2Decimal); + } + if (value !== _$unforgetCache[3].v) { + const _temp = _segment6(); + if (_temp !== _$unforgetNull) { + return _temp; + } + } + if (_$unforgetCache[2].n || valueWith2Decimal !== _$unforgetCache[1].v) { + _segment7(); + _$unforgetCache[2].e(derivedValue); + } + if (_$unforgetCache[4].n || _unwrappedJsxEl2 !== _$unforgetCache[5].v || _unwrappedJsxEl3 !== _$unforgetCache[6].v || _unwrappedJsxEl4 !== _$unforgetCache[7].v) { + _segment8(); + _$unforgetCache[4].e(_unwrappedJsxEl5); } - _$unforgetCommit(); return _unwrappedJsxEl5; }" `; + +exports[`Component fixtures applyModification fixture_10 1`] = ` +"import React, { useState, useEffect, useMemo, useCallback } from "react"; +const fetchUser = () => { + return fetch("https://api.github.com/users/mohebifar").then(response => { + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); + }); +}; +const fetchUserFollowers = () => { + return fetch("https://api.github.com/users/mohebifar/followers").then(response => { + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); + }); +}; +function UserList() { + const [_$unforgetCache, _$unforgetCommit, _$unforgetNull] = useCreateCache$unforget( + /* + 0 => _unwrappedJsxEl7 + 1 => _unwrappedJsxEl3 + 2 => _unwrappedJsxExp + 3 => user + 4 => _unwrappedJsxExp3 + 5 => handleUserClick + 6 => _unwrapped + 7 => setUser + 8 => _unwrappedJsxEl4 + 9 => _unwrappedJsxEl5 + 10 => toggleEvenOnes + 11 => setEvenOnes + 12 => _unwrapped2 + 13 => evenOnes + 14 => evenFollowers + 15 => userListElement + 16 => memoizedFollowers + 17 => followers + 18 => _unwrapped3 + 19 => setFollowers + 20 => _unwrappedJsxExp2 + 21 => _unwrappedJsxEl6 + */ + 22); + let _unwrapped = _$unforgetCache[6].v; + const _segment = () => { + _unwrapped = useState([]); + }; + let _unwrapped3 = _$unforgetCache[18].v; + const _segment2 = () => { + _unwrapped3 = useState([]); + }; + let [loading, setLoading]; + const _segment3 = () => { + [loading, setLoading] = useState(true); + }; + let [error, setError]; + const _segment4 = () => { + [error, setError] = useState(null); + }; + let _unwrapped2 = _$unforgetCache[12].v; + const _segment5 = () => { + _unwrapped2 = useState(false); + }; + const _segment6 = () => { + useEffect(() => { + Promise.all([fetchUser(), fetchUserFollowers(), new Promise(resolve => setTimeout(resolve, 2000))]).then(([userData, followersData]) => { + setFollowers(followersData); + setUser(userData); + setLoading(false); + }).catch(error => { + setError(error.toString()); + setLoading(false); + }); + }, []); + + // useCallback to memoize a hypothetical handler function + }; + let handleUserClick = _$unforgetCache[5].v; + const _segment7 = () => { + handleUserClick = useCallback(userId => { + console.log("User clicked:", userId); + // Handler logic here... + }, []); + }; // without useCallback + let toggleEvenOnes = _$unforgetCache[10].v; + const _segment8 = () => { + toggleEvenOnes = () => { + setEvenOnes(prev => !prev); + }; + }; + let _unwrappedJsxEl; + const _segment9 = () => { + _unwrappedJsxEl =
Loading...
; + }; + // Early return for loading state + const _segment10 = () => { + if (loading) return _unwrappedJsxEl; + + // Early return for error state + }; + let _unwrappedJsxEl2; + const _segment11 = () => { + _unwrappedJsxEl2 =
Error: {error}
; + }; + const _segment12 = () => { + if (error) return _unwrappedJsxEl2; + }; + let _unwrappedJsxExp2 = _$unforgetCache[20].v; + const _segment13 = () => { + _unwrappedJsxExp2 = evenOnes ? "Show Odd" : "Show Even"; + }; + let _unwrappedJsxEl4 = _$unforgetCache[8].v; + const _segment14 = () => { + _unwrappedJsxEl4 =

User List

; + }; + let _unwrappedJsxEl6 = _$unforgetCache[21].v; + const _segment15 = () => { + _unwrappedJsxEl6 =
    {userListElement}
; + }; + let setUser = _$unforgetCache[7].v; + const _segment16 = () => { + setUser = _unwrapped[1]; + }; + let user = _$unforgetCache[3].v; + const _segment17 = () => { + user = _unwrapped[0]; + }; + let setFollowers = _$unforgetCache[19].v; + const _segment18 = () => { + setFollowers = _unwrapped3[1]; + }; + let followers = _$unforgetCache[17].v; + const _segment19 = () => { + followers = _unwrapped3[0]; + }; + let setEvenOnes = _$unforgetCache[11].v; + const _segment20 = () => { + setEvenOnes = _unwrapped2[1]; + }; + let evenOnes = _$unforgetCache[13].v; + const _segment21 = () => { + evenOnes = _unwrapped2[0]; + }; + let memoizedFollowers = _$unforgetCache[16].v; + const _segment22 = () => { + memoizedFollowers = useMemo(() => followers, [followers]); + }; + let evenFollowers = _$unforgetCache[14].v; + const _segment23 = () => { + evenFollowers = memoizedFollowers.filter((_, index) => index % 2 === (evenOnes ? 0 : 1)); + }; + let userListElement = _$unforgetCache[15].v; + const _segment24 = () => { + userListElement = evenFollowers.map(follower => ); + }; + let _unwrappedJsxExp = _$unforgetCache[2].v; + const _segment25 = () => { + _unwrappedJsxExp = user.name; + }; + let _unwrappedJsxExp3 = _$unforgetCache[4].v; + const _segment26 = () => { + _unwrappedJsxExp3 = () => handleUserClick(user.id); + }; + let _unwrappedJsxEl3 = _$unforgetCache[1].v; + const _segment27 = () => { + _unwrappedJsxEl3 =

Follwers of {_unwrappedJsxExp}

; + }; + let _unwrappedJsxEl5 = _$unforgetCache[9].v; + const _segment28 = () => { + _unwrappedJsxEl5 = ; + }; + let _unwrappedJsxEl7 = _$unforgetCache[0].v; + const _segment29 = () => { + _unwrappedJsxEl7 =
+ {_unwrappedJsxEl3} + {_unwrappedJsxEl4} + {_unwrappedJsxEl5} + {_unwrappedJsxEl6} +
; + }; + _segment(); + _$unforgetCache[6].e(_unwrapped); + _segment2(); + _$unforgetCache[18].e(_unwrapped3); + _segment3(); + _segment4(); + _segment5(); + _$unforgetCache[12].e(_unwrapped2); + _segment6(); + _segment7(); + _$unforgetCache[5].e(handleUserClick); + if (_$unforgetCache[10].n) { + _segment8(); + _$unforgetCache[10].e(toggleEvenOnes); + } + _segment9(); + _segment10(); + _segment11(); + _segment12(); + if (_$unforgetCache[20].n) { + _segment13(); + _$unforgetCache[20].e(_unwrappedJsxExp2); + } + if (_$unforgetCache[8].n) { + _segment14(); + _$unforgetCache[8].e(_unwrappedJsxEl4); + } + if (_$unforgetCache[21].n) { + _segment15(); + _$unforgetCache[21].e(_unwrappedJsxEl6); + } + if (_$unforgetCache[7].n || _unwrapped !== _$unforgetCache[6].v) { + _segment16(); + _$unforgetCache[7].e(setUser); + } + if (_$unforgetCache[3].n || _unwrapped !== _$unforgetCache[6].v) { + _segment17(); + _$unforgetCache[3].e(user); + } + if (_$unforgetCache[19].n || _unwrapped3 !== _$unforgetCache[18].v) { + _segment18(); + _$unforgetCache[19].e(setFollowers); + } + if (_$unforgetCache[17].n || _unwrapped3 !== _$unforgetCache[18].v) { + _segment19(); + _$unforgetCache[17].e(followers); + } + if (_$unforgetCache[11].n || _unwrapped2 !== _$unforgetCache[12].v) { + _segment20(); + _$unforgetCache[11].e(setEvenOnes); + } + if (_$unforgetCache[13].n || _unwrapped2 !== _$unforgetCache[12].v) { + _segment21(); + _$unforgetCache[13].e(evenOnes); + } + _segment22(); + _$unforgetCache[16].e(memoizedFollowers); + if (_$unforgetCache[14].n || memoizedFollowers !== _$unforgetCache[16].v || evenOnes !== _$unforgetCache[13].v) { + _segment23(); + _$unforgetCache[14].e(evenFollowers); + } + if (_$unforgetCache[15].n || evenFollowers !== _$unforgetCache[14].v) { + _segment24(); + _$unforgetCache[15].e(userListElement); + } + if (_$unforgetCache[2].n || user.name !== _$unforgetCache[3].v.name) { + _segment25(); + _$unforgetCache[2].e(_unwrappedJsxExp); + } + if (_$unforgetCache[4].n || handleUserClick !== _$unforgetCache[5].v || user !== _$unforgetCache[3].v) { + _segment26(); + _$unforgetCache[4].e(_unwrappedJsxExp3); + } + if (_$unforgetCache[1].n || _unwrappedJsxExp !== _$unforgetCache[2].v) { + _segment27(); + _$unforgetCache[1].e(_unwrappedJsxEl3); + } + if (_$unforgetCache[9].n || toggleEvenOnes !== _$unforgetCache[10].v || evenOnes !== _$unforgetCache[13].v || _unwrappedJsxExp2 !== _$unforgetCache[20].v) { + _segment28(); + _$unforgetCache[9].e(_unwrappedJsxEl5); + } + if (_$unforgetCache[0].n || _unwrappedJsxEl3 !== _$unforgetCache[1].v || _unwrappedJsxExp3 !== _$unforgetCache[4].v || _unwrappedJsxEl4 !== _$unforgetCache[8].v || _unwrappedJsxEl5 !== _$unforgetCache[9].v || userListElement !== _$unforgetCache[15].v || _unwrappedJsxEl6 !== _$unforgetCache[21].v) { + _segment29(); + _$unforgetCache[0].e(_unwrappedJsxEl7); + } + return _unwrappedJsxEl7; +} +function UserListItem(_props) { + const [_$unforgetCache2, _$unforgetCommit2, _$unforgetNull2] = useCreateCache$unforget( + /* + 0 => _unwrappedJsxEl10 + 1 => _unwrappedJsxExp4 + 2 => _unwrappedJsxEl8 + 3 => _unwrappedJsxExp5 + 4 => avatar_url + 5 => _props + 6 => login + 7 => html_url + 8 => _unwrappedJsxEl9 + */ + 9); + let _unwrappedJsxExp4 = _$unforgetCache2[1].v; + const _segment46 = () => { + _unwrappedJsxExp4 = { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + gap: 10, + marginBottom: 10 + }; + }; + let _unwrappedJsxExp5 = _$unforgetCache2[3].v; + const _segment47 = () => { + _unwrappedJsxExp5 = { + width: 100, + height: 100, + borderRadius: 20 + }; + }; + let _unwrappedJsxEl9 = _$unforgetCache2[8].v; + const _segment48 = () => { + _unwrappedJsxEl9 = {login}; + }; + let html_url = _$unforgetCache2[7].v; + const _segment49 = () => { + html_url = _props.user.html_url; + }; + let avatar_url = _$unforgetCache2[4].v; + const _segment50 = () => { + avatar_url = _props.user.avatar_url; + }; + let login = _$unforgetCache2[6].v; + const _segment51 = () => { + login = _props.user.login; + }; + let _unwrappedJsxEl8 = _$unforgetCache2[2].v; + const _segment52 = () => { + _unwrappedJsxEl8 = {login}; + }; + let _unwrappedJsxEl10 = _$unforgetCache2[0].v; + const _segment53 = () => { + _unwrappedJsxEl10 =
  • + {_unwrappedJsxEl8} + {_unwrappedJsxEl9} +
  • ; + }; + if (_$unforgetCache2[1].n) { + _segment46(); + _$unforgetCache2[1].e(_unwrappedJsxExp4); + } + if (_$unforgetCache2[3].n) { + _segment47(); + _$unforgetCache2[3].e(_unwrappedJsxExp5); + } + if (_$unforgetCache2[8].n) { + _segment48(); + _$unforgetCache2[8].e(_unwrappedJsxEl9); + } + if (_$unforgetCache2[7].n || _props !== _$unforgetCache2[5].v) { + _segment49(); + _$unforgetCache2[7].e(html_url); + } + if (_$unforgetCache2[4].n || _props !== _$unforgetCache2[5].v) { + _segment50(); + _$unforgetCache2[4].e(avatar_url); + } + if (_$unforgetCache2[6].n || _props !== _$unforgetCache2[5].v) { + _segment51(); + _$unforgetCache2[6].e(login); + } + if (_$unforgetCache2[2].n || _unwrappedJsxExp5 !== _$unforgetCache2[3].v || avatar_url !== _$unforgetCache2[4].v || login !== _$unforgetCache2[6].v) { + _segment52(); + _$unforgetCache2[2].e(_unwrappedJsxEl8); + } + if (_$unforgetCache2[0].n || _unwrappedJsxExp4 !== _$unforgetCache2[1].v || _unwrappedJsxEl8 !== _$unforgetCache2[2].v || login !== _$unforgetCache2[6].v || html_url !== _$unforgetCache2[7].v || _unwrappedJsxEl9 !== _$unforgetCache2[8].v) { + _segment53(); + _$unforgetCache2[0].e(_unwrappedJsxEl10); + } + return _unwrappedJsxEl10; +} +export default UserList;" +`; + +exports[`Component fixtures applyModification fixture_11 1`] = ` +"const useMovies = () => { + return { + data: [{ + title: "The Shawshank Redemption", + year: 1994 + }, { + title: "The Godfather", + year: 1972 + }, { + title: "The Dark Knight", + year: 2008 + }], + loading: false + }; +}; +export function MyComponent() { + const [_$unforgetCache, _$unforgetCommit, _$unforgetNull] = useCreateCache$unforget( + /* + 0 => _unwrappedJsxEl + 1 => _unwrappedJsxEl2 + 2 => _unwrappedJsxExp + 3 => filteredMovies + 4 => i + */ + 5); + let i = _$unforgetCache[4].v; + const _segment = () => { + i = []; + }; + const _segment2 = () => { + i = [{ + title: "The Shawshank Redemption", + year: 1994 + }]; + }; + let { + movies, + loading + }; + const _segment3 = () => { + ({ + movies, + loading + } = useMovies()); + }; + const _segment4 = () => { + if (loading) { + let _unwrappedJsxEl = _$unforgetCache[0].v; + const _segment11 = () => { + _unwrappedJsxEl =
    Loading...
    ; + }; + if (_$unforgetCache[0].n) { + _segment11(); + _$unforgetCache[0].e(_unwrappedJsxEl); + } + return _unwrappedJsxEl; + } + }; + const _segment5 = () => { + for (let i = 0; i < movies.length; i++) { + const _segment13 = () => { + if (movies[i].year > 2000) { + const _segment14 = () => { + filteredMovies.push(movies[i]); + }; + let i = []; + const filteredMovies = i.concat([]); + if (filteredMovies !== _$unforgetCache[3].v) { + _segment14(); + } + } + }; + _segment13(); + } + }; + let filteredMovies = _$unforgetCache[3].v; + const _segment6 = () => { + filteredMovies = i.concat([]); + }; + let _unwrappedJsxExp = _$unforgetCache[2].v; + const _segment7 = () => { + _unwrappedJsxExp = filteredMovies.map(movie =>
    + {movie.title} + {/* {value} */} +
    ); + }; + let _unwrappedJsxEl2 = _$unforgetCache[1].v; + const _segment8 = () => { + _unwrappedJsxEl2 =
    + {_unwrappedJsxExp} +
    ; + }; + if (_$unforgetCache[4].n) { + _segment(); + _$unforgetCache[4].e(i); + } + _segment2(); + _segment3(); + const _temp = _segment4(); + if (_temp !== _$unforgetNull) { + return _temp; + } + _segment5(); + if (_$unforgetCache[3].n || i !== _$unforgetCache[4].v || filteredMovies !== _$unforgetCache[3].v) { + _segment6(); + _$unforgetCache[3].e(filteredMovies); + } + if (_$unforgetCache[2].n || filteredMovies !== _$unforgetCache[3].v) { + _segment7(); + _$unforgetCache[2].e(_unwrappedJsxExp); + } + if (_$unforgetCache[1].n || _unwrappedJsxExp !== _$unforgetCache[2].v) { + _segment8(); + _$unforgetCache[1].e(_unwrappedJsxEl2); + } + return _unwrappedJsxEl2; +}" +`; + +exports[`Component fixtures applyModification fixture_12 1`] = ` +"export function MyComponent() { + const [_$unforgetCache, _$unforgetCommit, _$unforgetNull] = useCreateCache$unforget( + /* + 0 => _unwrappedJsxEl + 1 => value + */ + 2); + let value = _$unforgetCache[1].v; + const _segment = () => { + value = "user"; + }; + let _unwrappedJsxEl = _$unforgetCache[0].v; + const _segment2 = () => { + _unwrappedJsxEl =
    Loading {value}...
    ; + }; + if (_$unforgetCache[1].n) { + _segment(); + _$unforgetCache[1].e(value); + } + if (_$unforgetCache[0].n || value !== _$unforgetCache[1].v) { + _segment2(); + _$unforgetCache[0].e(_unwrappedJsxEl); + } + return _unwrappedJsxEl; + console.log("unreachable"); + return null; +}" +`; diff --git a/packages/compiler/src/fixtures/fixture_1.js b/packages/compiler/src/fixtures/fixture_1.js index 5174949..23a3d6c 100644 --- a/packages/compiler/src/fixtures/fixture_1.js +++ b/packages/compiler/src/fixtures/fixture_1.js @@ -1,9 +1,16 @@ export function MyComponent() { - const [state, setState] = useState(0); + const value = []; - const myDerivedVariable = state + 1; + let i = 2; + const k = "hi"; - const unusedVariable = 1; + value.push(...["Hello", "World", i]); - return
    {myDerivedVariable}
    ; + const m = i; + + if (m === 2) { + return null; + } + + return
    {value[0]}
    ; } diff --git a/packages/compiler/src/fixtures/fixture_10.js b/packages/compiler/src/fixtures/fixture_10.js new file mode 100644 index 0000000..e64cd64 --- /dev/null +++ b/packages/compiler/src/fixtures/fixture_10.js @@ -0,0 +1,111 @@ +import React, { useState, useEffect, useMemo, useCallback } from "react"; + +const fetchUser = () => { + return fetch("https://api.github.com/users/mohebifar").then((response) => { + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); + }); +}; + +const fetchUserFollowers = () => { + return fetch("https://api.github.com/users/mohebifar/followers").then( + (response) => { + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); + } + ); +}; + +function UserList() { + const [user, setUser] = useState([]); + const [followers, setFollowers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [evenOnes, setEvenOnes] = useState(false); + + useEffect(() => { + Promise.all([ + fetchUser(), + fetchUserFollowers(), + new Promise((resolve) => setTimeout(resolve, 2000)), + ]) + .then(([userData, followersData]) => { + setFollowers(followersData); + setUser(userData); + setLoading(false); + }) + .catch((error) => { + setError(error.toString()); + setLoading(false); + }); + }, []); + + // useCallback to memoize a hypothetical handler function + const handleUserClick = useCallback((userId) => { + console.log("User clicked:", userId); + // Handler logic here... + }, []); + + // without useCallback + const toggleEvenOnes = () => { + setEvenOnes((prev) => !prev); + }; + + const memoizedFollowers = useMemo(() => followers, [followers]); + + const evenFollowers = memoizedFollowers.filter( + (_, index) => index % 2 === (evenOnes ? 0 : 1) + ); + + // Early return for loading state + if (loading) return
    Loading...
    ; + + // Early return for error state + if (error) return
    Error: {error}
    ; + + const userListElement = evenFollowers.map((follower) => ( + + )); + + return ( +
    +

    Follwers of {user.name}

    +

    User List

    + +
      handleUserClick(user.id)}>{userListElement}
    +
    + ); +} + +function UserListItem({ user: { login, avatar_url, html_url } }) { + return ( +
  • + {login} + {login} +
  • + ); +} + +export default UserList; diff --git a/packages/compiler/src/fixtures/fixture_11.js b/packages/compiler/src/fixtures/fixture_11.js new file mode 100644 index 0000000..e4c8fe3 --- /dev/null +++ b/packages/compiler/src/fixtures/fixture_11.js @@ -0,0 +1,41 @@ +const useMovies = () => { + return { + data: [ + { title: "The Shawshank Redemption", year: 1994 }, + { title: "The Godfather", year: 1972 }, + { title: "The Dark Knight", year: 2008 }, + ], + loading: false, + }; +}; + +export function MyComponent() { + let i = []; + + const filteredMovies = i.concat([]); + + i = [{ title: "The Shawshank Redemption", year: 1994 }]; + + const { movies, loading } = useMovies(); + + if (loading) { + return
    Loading...
    ; + } + + for (let i = 0; i < movies.length; i++) { + if (movies[i].year > 2000) { + filteredMovies.push(movies[i]); + } + } + + return ( +
    + {filteredMovies.map((movie) => ( +
    + {movie.title} + {/* {value} */} +
    + ))} +
    + ); +} diff --git a/packages/compiler/src/fixtures/fixture_12.js b/packages/compiler/src/fixtures/fixture_12.js new file mode 100644 index 0000000..1365ab1 --- /dev/null +++ b/packages/compiler/src/fixtures/fixture_12.js @@ -0,0 +1,8 @@ +export function MyComponent() { + const value = "user"; + return
    Loading {value}...
    ; + + console.log("unreachable"); + + return null; +} diff --git a/packages/compiler/src/fixtures/fixture_5.js b/packages/compiler/src/fixtures/fixture_5.js new file mode 100644 index 0000000..18de669 --- /dev/null +++ b/packages/compiler/src/fixtures/fixture_5.js @@ -0,0 +1,90 @@ +const words = [ + "Shawshank", + "Redemption", + "Godfather", + "Dark", + "Knight", + "Forest", + "Gump", + "Private", + "Ryan", +]; + +const initalMovies = [ + { + title: "The Shawshank Redemption", + year: 1994, + }, + { + title: "The Godfather", + year: 1972, + }, + { + title: "The Dark Knight", + year: 2008, + }, +]; + +const useMovies = () => { + const [movies, setMovies] = useState(initalMovies); + + const addRandomMovie = () => { + setMovies((p) => [ + ...p, + { + title: + "The " + + words[Math.floor(Math.random() * words.length)] + + " " + + words[Math.floor(Math.random() * words.length)], + year: 1990 + Math.floor(Math.random() * 20), + }, + ]); + }; + + return [movies, addRandomMovie]; +}; + +const useKey = () => "key1"; + +const object = { key1: "value1", key2: "value2" }; + +export function MyComponent() { + const [movies, addRandomMovie] = useMovies(); + + const key = useKey(); + + const value = object[key]; + + const filteredMovies = []; + + const i = 5; + + for (let i = 0; i < movies.length; i++) { + if (movies[i].year > 2000) { + filteredMovies.push(movies[i]); + } + } + + if (filteredMovies.length > 0) { + // Just some side effect + console.log("Movies after 2000: ", filteredMovies); + } + + console.log("Total number of movies: ", movies.length); + + return ( +
    + +
    total number of movies: {movies.length}
    + {filteredMovies.map((movie) => ( +
    + {movie.title} + {value} +
    + ))} +
    + ); +} + +export default MyComponent; diff --git a/packages/compiler/src/fixtures/fixture_6.js b/packages/compiler/src/fixtures/fixture_6.js new file mode 100644 index 0000000..858900e --- /dev/null +++ b/packages/compiler/src/fixtures/fixture_6.js @@ -0,0 +1,3 @@ +export function SimpleJSX() { + return
    ; +} diff --git a/packages/compiler/src/fixtures/fixture_7.js b/packages/compiler/src/fixtures/fixture_7.js new file mode 100644 index 0000000..3bee389 --- /dev/null +++ b/packages/compiler/src/fixtures/fixture_7.js @@ -0,0 +1,10 @@ +export function SimpleJSX() { + const value = useValue(); + + return ( +
    + + {value} +
    + ); +} diff --git a/packages/compiler/src/fixtures/fixture_8.js b/packages/compiler/src/fixtures/fixture_8.js new file mode 100644 index 0000000..378be91 --- /dev/null +++ b/packages/compiler/src/fixtures/fixture_8.js @@ -0,0 +1,9 @@ +export function SimpleJSX() { + const value = useValue(); + + if (value === "loading") { + return
    Loading...
    ; + } + + return ; +} diff --git a/packages/compiler/src/fixtures/fixture_9.js b/packages/compiler/src/fixtures/fixture_9.js new file mode 100644 index 0000000..8b7f946 --- /dev/null +++ b/packages/compiler/src/fixtures/fixture_9.js @@ -0,0 +1,31 @@ +function useValue() { + const [value, setValue] = useState(Math.random()); + useEffect(() => { + const interval = setInterval(() => { + setValue(Math.random()); + }, 1000); + + return () => clearInterval(interval); + }, []); + return value; +} + +export function SimpleJSX() { + const value = useValue(); + + const valueWith2Decimal = value.toFixed(2); + + if (value > 0.8) { + return
    Loading because value is {valueWith2Decimal}...
    ; + } + + const derivedValue = "state updated to: " + valueWith2Decimal; + + return ( +
    + +
    {value}
    +
    {derivedValue}
    +
    + ); +} diff --git a/packages/compiler/src/utils/constants.ts b/packages/compiler/src/utils/constants.ts index bbf87e2..9d8f3ac 100644 --- a/packages/compiler/src/utils/constants.ts +++ b/packages/compiler/src/utils/constants.ts @@ -2,12 +2,33 @@ export const RUNTIME_MODULE_NAME = "@react-unforget/runtime"; export const RUNTIME_MODULE_CREATE_CACHE_HOOK_NAME = "useCreateCache$unforget"; export const DEFAULT_CACHE_VARIABLE_NAME = "$unforgetCache"; export const DEFAULT_CACHE_COMMIT_VARIABLE_NAME = "$unforgetCommit"; +export const DEFAULT_CACHE_NULL_VARIABLE_NAME = "$unforgetNull"; export const DEFAULT_UNUSED_VARIABLE_NAME = "_unused"; export const DEFAULT_UNWRAPPED_VARIABLE_NAME = "_unwrapped"; -export const DEFAULT_UNWRAPPED_JSX_EXPRESSION_VARIABLE_NAME = "_unwrappedJsxExp"; +export const DEFAULT_UNWRAPPED_JSX_EXPRESSION_VARIABLE_NAME = + "_unwrappedJsxExp"; +export const DEFAULT_UNWRAPPED_PROPS_VARIABLE_NAME = "_props"; export const DEFAULT_UNWRAPPED_JSX_ELEMENT_VARIABLE_NAME = "_unwrappedJsxEl"; +export const DEFAULT_SIDE_EFFECT_VARIABLE_NAME = "_sideEffect"; +export const DEFAULT_SEGMENT_CALLABLE_VARIABLE_NAME = "_segment"; export const RUNTIME_MODULE_CACHE_ENQUEUE_METHOD_NAME = "e"; export const RUNTIME_MODULE_CACHE_VALUE_PROP_NAME = "v"; export const RUNTIME_MODULE_CACHE_IS_NOT_SET_PROP_NAME = "n"; + +export const MUTATING_METHODS = [ + "push", + "pop", + "shift", + "unshift", + "splice", + "sort", + "reverse", + "copyWithin", + "fill", + "set", + "delete", + "add", + "clear", +]; diff --git a/packages/compiler/src/utils/errors/CircularDependencyError.ts b/packages/compiler/src/utils/errors/CircularDependencyError.ts new file mode 100644 index 0000000..da414b5 --- /dev/null +++ b/packages/compiler/src/utils/errors/CircularDependencyError.ts @@ -0,0 +1,10 @@ +export class CircularDependencyError extends Error { + constructor(from: babel.NodePath, to: babel.NodePath) { + super( + "Circular dependency detected - from: " + + from.toString() + + " to: " + + to.toString() + ); + } +} diff --git a/packages/compiler/src/utils/errors/LeftmostIdNotFound.ts b/packages/compiler/src/utils/errors/LeftmostIdNotFound.ts new file mode 100644 index 0000000..c17cd87 --- /dev/null +++ b/packages/compiler/src/utils/errors/LeftmostIdNotFound.ts @@ -0,0 +1,7 @@ +import type * as t from "@babel/types"; + +export class LeftmostIdNotFound extends Error { + constructor(path: t.Node) { + super("Could not find leftmost identifier name for" + path.toString()); + } +} diff --git a/packages/compiler/src/utils/errors/RightmostIdNotFound.ts b/packages/compiler/src/utils/errors/RightmostIdNotFound.ts index 502c8bc..4ed1263 100644 --- a/packages/compiler/src/utils/errors/RightmostIdNotFound.ts +++ b/packages/compiler/src/utils/errors/RightmostIdNotFound.ts @@ -1,7 +1,7 @@ -import type * as babel from "@babel/core"; +import * as t from "@babel/types"; export class RightmostIdNotFound extends Error { - constructor(path: babel.NodePath) { + constructor(path: t.Node) { super("Could not find rightmost identifier name for" + path.toString()); } } diff --git a/packages/compiler/src/utils/find-mutating-expression.ts b/packages/compiler/src/utils/find-mutating-expression.ts new file mode 100644 index 0000000..ac8f478 --- /dev/null +++ b/packages/compiler/src/utils/find-mutating-expression.ts @@ -0,0 +1,49 @@ +import { MUTATING_METHODS } from "./constants"; +import { getDeclaredIdentifiersInLVal } from "./get-declared-identifiers-in-lval"; +import { getLeftmostIdName } from "./get-leftmost-id-name"; +import { getRightmostIdName } from "./get-rightmost-id-name"; + +export function findMutatingExpression( + path: babel.NodePath, + name: string +) { + const result = path.find((innerPath) => { + if (innerPath.isStatement()) { + return true; + } + + if (innerPath.isAssignmentExpression()) { + const left = innerPath.node.left; + const leftMostIds = innerPath.isLVal() + ? getDeclaredIdentifiersInLVal(innerPath) + : [getLeftmostIdName(left)]; + + return leftMostIds.includes(name); + } + + if (innerPath.isUpdateExpression()) { + return getLeftmostIdName(innerPath.node.argument) === name; + } + + if (innerPath.isCallExpression()) { + const callee = innerPath.get("callee"); + + if (callee.isMemberExpression()) { + const leftMostId = getLeftmostIdName(callee.node); + const rightmostId = getRightmostIdName(callee.node); + + if (MUTATING_METHODS.includes(rightmostId)) { + return leftMostId === name; + } + } + } + + return false; + }); + + if (result?.isStatement()) { + return null; + } + + return result as babel.NodePath; +} diff --git a/packages/compiler/src/utils/get-block-statements-of-path.ts b/packages/compiler/src/utils/get-block-statements-of-path.ts new file mode 100644 index 0000000..9ffbed8 --- /dev/null +++ b/packages/compiler/src/utils/get-block-statements-of-path.ts @@ -0,0 +1,98 @@ +export function getBlockStatementsOfPath( + path: babel.NodePath +): babel.NodePath[] { + if (path.isBlockStatement()) { + return [path]; + } + + if (path.isIfStatement()) { + const consequent = path.get("consequent"); + const alternate = path.get("alternate"); + + if (consequent.isBlockStatement()) { + return [consequent]; + } + + if (consequent.isIfStatement()) { + return getBlockStatementsOfPath(consequent); + } + + if (alternate && alternate.isBlockStatement()) { + return [alternate]; + } + + if (alternate && alternate.isIfStatement()) { + return getBlockStatementsOfPath(alternate); + } + } + + if (path.isWhileStatement()) { + const body = path.get("body"); + + if (body.isBlockStatement()) { + return [body]; + } + } + + if (path.isDoWhileStatement()) { + const body = path.get("body"); + + if (body.isBlockStatement()) { + return [body]; + } + } + + if (path.isForStatement()) { + const body = path.get("body"); + + if (body.isBlockStatement()) { + return [body]; + } + } + + if (path.isForInStatement()) { + const body = path.get("body"); + + if (body.isBlockStatement()) { + return [body]; + } + } + + if (path.isForOfStatement()) { + const body = path.get("body"); + + if (body.isBlockStatement()) { + return [body]; + } + } + + if (path.isSwitchStatement()) { + const cases = path.get("cases"); + + const blockStatements: babel.NodePath[] = []; + + cases.forEach((c) => { + const consequent = c.get("consequent"); + + if (consequent.length > 0) { + consequent.forEach((consequentStatement) => { + if (consequentStatement.isBlockStatement()) { + blockStatements.push(consequentStatement); + } + }); + } + }); + + return blockStatements; + } + + if (path.isTryStatement()) { + const block = path.get("block"); + + if (block.isBlockStatement()) { + return [block]; + } + } + + return []; +} diff --git a/packages/compiler/src/utils/get-leftmost-id-name.ts b/packages/compiler/src/utils/get-leftmost-id-name.ts new file mode 100644 index 0000000..2651bba --- /dev/null +++ b/packages/compiler/src/utils/get-leftmost-id-name.ts @@ -0,0 +1,16 @@ +import * as t from "@babel/types"; +import { LeftmostIdNotFound } from "./errors/LeftmostIdNotFound"; + +export function getLeftmostIdName( + node: babel.types.LVal | babel.types.Expression +): string { + if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) { + const object = node.object; + + return getLeftmostIdName(object); + } else if (t.isIdentifier(node)) { + return node.name; + } + + throw new LeftmostIdNotFound(node); +} diff --git a/packages/compiler/src/utils/get-referenced-variables-inside.ts b/packages/compiler/src/utils/get-referenced-variables-inside.ts index b274ea6..11ecea2 100644 --- a/packages/compiler/src/utils/get-referenced-variables-inside.ts +++ b/packages/compiler/src/utils/get-referenced-variables-inside.ts @@ -5,24 +5,20 @@ import { isReferenceIdentifier } from "./is-reference-identifier"; export function getReferencedVariablesInside( path: babel.NodePath ) { - const references = new Set(); + const map = new Map, Binding>(); path.traverse({ Identifier(innerPath) { if (isReferenceIdentifier(innerPath)) { - references.add(innerPath.node.name); + const name = innerPath.node.name; + + const binding = path.scope.getBinding(name); + if (binding) { + map.set(innerPath, binding); + } } }, }); - const bindings: Binding[] = []; - - references.forEach((name) => { - const binding = path.scope.getBinding(name); - if (binding) { - bindings.push(binding); - } - }); - - return bindings; + return map; } diff --git a/packages/compiler/src/utils/get-returns-of-function.ts b/packages/compiler/src/utils/get-returns-of-function.ts index a019f15..24f379d 100644 --- a/packages/compiler/src/utils/get-returns-of-function.ts +++ b/packages/compiler/src/utils/get-returns-of-function.ts @@ -1,12 +1,12 @@ import type * as babel from "@babel/core"; -import { getFunctionParent } from "./get-function-parent"; +import { isInTheSameFunctionScope } from "./is-in-the-same-function-scope"; export function getReturnsOfFunction(fn: babel.NodePath) { const returns: babel.NodePath[] = []; fn.traverse({ ReturnStatement(path) { - if (getFunctionParent(path)?.node === fn.node) { + if (isInTheSameFunctionScope(path, fn)) { returns.push(path); } }, diff --git a/packages/compiler/src/utils/get-rightmost-id-name.ts b/packages/compiler/src/utils/get-rightmost-id-name.ts index 8f6061c..0835e64 100644 --- a/packages/compiler/src/utils/get-rightmost-id-name.ts +++ b/packages/compiler/src/utils/get-rightmost-id-name.ts @@ -1,24 +1,22 @@ -import type * as babel from "@babel/core"; +import * as t from "@babel/types"; import { RightmostIdNotFound } from "./errors/RightmostIdNotFound"; export function getRightmostIdName( - path: babel.NodePath< - babel.types.Expression | babel.types.V8IntrinsicIdentifier - > + node: t.Expression | t.V8IntrinsicIdentifier ): string { - if (path.isMemberExpression()) { - const property = path.get("property"); + if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) { + const property = node.property; - if (property.isStringLiteral()) { - return property.node.value; - } else if (property.isIdentifier() && !path.node.computed) { - return property.node.name; - } else if (property.isNumericLiteral()) { - return property.node.value.toString(); + if (t.isStringLiteral(property)) { + return property.value; + } else if (t.isIdentifier(property) && !node.computed) { + return property.name; + } else if (t.isNumericLiteral(property)) { + return property.value.toString(); } - } else if (path.isIdentifier()) { - return path.node.name; + } else if (t.isIdentifier(node)) { + return node.name; } - throw new RightmostIdNotFound(path); + throw new RightmostIdNotFound(node); } diff --git a/packages/compiler/src/utils/is-hook-call.ts b/packages/compiler/src/utils/is-hook-call.ts index 3de5283..d36fa3d 100644 --- a/packages/compiler/src/utils/is-hook-call.ts +++ b/packages/compiler/src/utils/is-hook-call.ts @@ -1,5 +1,10 @@ import type * as babel from "@babel/core"; import { getRightmostIdName } from "./get-rightmost-id-name"; +import { isInTheSameFunctionScope } from "./is-in-the-same-function-scope"; + +export function doesMatchHookName(name: string) { + return /^use[A-Z]/.test(name); +} export function isHookCall(path: babel.NodePath) { path.assertCallExpression(); @@ -9,12 +14,32 @@ export function isHookCall(path: babel.NodePath) { let rightmostId = ""; try { - rightmostId = getRightmostIdName(callee); + rightmostId = getRightmostIdName(callee.node); } catch { // We pessimistically assume that it's a hook if we can't identify the rightmost id // TODO: Make this configurable / throw an error / or log a warning return true; } - return /^use[A-Z]/.test(rightmostId); + return doesMatchHookName(rightmostId); +} + +export function hasHookCall( + path: babel.NodePath, + componentPath: babel.NodePath +) { + let hasHookCall = false; + path.traverse({ + CallExpression: (innerPath) => { + if ( + isHookCall(innerPath) && + isInTheSameFunctionScope(innerPath, componentPath) + ) { + hasHookCall = true; + return; + } + }, + }); + + return hasHookCall; } diff --git a/packages/compiler/src/utils/is-in-the-same-function-scope.ts b/packages/compiler/src/utils/is-in-the-same-function-scope.ts new file mode 100644 index 0000000..0104507 --- /dev/null +++ b/packages/compiler/src/utils/is-in-the-same-function-scope.ts @@ -0,0 +1,19 @@ +import * as babel from "@babel/core"; +import * as t from "@babel/types"; +import { Scope } from "@babel/traverse"; + +// We need this to properly detect if return statements belong to the same function +export function isInTheSameFunctionScope( + path: babel.NodePath, + fn: babel.NodePath +) { + let currentScope: Scope | null = path.scope; + do { + if (t.isFunction(currentScope.block)) { + return currentScope.block === fn.node; + } + currentScope = currentScope?.parent ?? null; +} while (currentScope); + + return false; +} diff --git a/packages/compiler/src/utils/is-variable-in-scope-of.ts b/packages/compiler/src/utils/is-variable-in-scope-of.ts new file mode 100644 index 0000000..3b81508 --- /dev/null +++ b/packages/compiler/src/utils/is-variable-in-scope-of.ts @@ -0,0 +1,7 @@ +import { Binding, Scope } from "@babel/traverse"; + +export function isVariableInScopeOf(binding: Binding, scope: Scope) { + const name = binding.identifier.name; + + return scope.getBinding(name) === binding; +} diff --git a/packages/compiler/src/utils/member-expression-to-dot-notation.ts b/packages/compiler/src/utils/member-expression-to-dot-notation.ts deleted file mode 100644 index 87bc8ff..0000000 --- a/packages/compiler/src/utils/member-expression-to-dot-notation.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as t from "@babel/types"; - -// Convert a member expression to dot notation -// This is used to make an identifier from a member expression -export function memberExpressionToDotNotation(node: t.Expression) { - // Recursive function to handle nested member expressions - function traverse(node: t.Expression | t.PrivateName): string { - if (node.type === "ThisExpression") { - return "this"; - } else if (node.type === "Identifier") { - // Base case: if the node is an identifier, prepend its name to the dotNotation string - return node.name; - } else if (t.isLiteral(node)) { - switch (node.type) { - case "StringLiteral": - return `"${node.value}"`; - case "BigIntLiteral": - return `${node.value}n`; - case "NullLiteral": - return "null"; - case "TemplateLiteral": - return `\`${node.quasis?.[0]?.value.raw}\``; - case "RegExpLiteral": - return `/${node.pattern}/${node.flags}`; - default: - return String(node.value); - } - } else if (node.type === "MemberExpression") { - // Handle non-computed member expressions - - const propsPrefix = node.computed ? `[` : "."; - const propsSuffix = node.computed ? `]` : ""; - - const propsNotation = propsPrefix + traverse(node.property) + propsSuffix; - - return traverse(node.object) + propsNotation; - } - - throw new Error("Node type not supported"); - } - - // Start the traversal with the given node - return traverse(node); -} diff --git a/packages/compiler/src/utils/reorder-by-topology.ts b/packages/compiler/src/utils/reorder-by-topology.ts new file mode 100644 index 0000000..e3490a0 --- /dev/null +++ b/packages/compiler/src/utils/reorder-by-topology.ts @@ -0,0 +1,57 @@ +import { ComponentMutableSegment } from "~/classes/ComponentMutableSegment"; +import { CircularDependencyError } from "./errors/CircularDependencyError"; + +type StatementPath = babel.NodePath; + +export function reorderByTopology( + statements: StatementPath[] | Set, + map: Map +) { + const stack: StatementPath[] = []; + const visited = new Set(); + const recursionStack = new Set(); + + function dfs(statement: StatementPath) { + if (visited.has(statement)) { + return; + } + + visited.add(statement); + recursionStack.add(statement); + + const dependencies = map.get(statement)?.getDependencies(); + + // Visit all the dependent nodes + dependencies?.forEach(({ componentVariable }) => { + const dependencyStatement = componentVariable.getParentStatement()!; + // If the dependent node is in the recursion stack, we have a cycle + if (dependencyStatement === statement) { + return; + } + + if (recursionStack.has(dependencyStatement)) { + throw new CircularDependencyError(statement, dependencyStatement); + } + + dfs(dependencyStatement); + }); + + stack.push(statement); + recursionStack.delete(statement); + } + + statements.forEach((statement) => { + if (map.get(statement)?.getDependencies().size === 0) { + stack.push(statement); + visited.add(statement); + } + }); + + statements.forEach((statement) => { + if (!visited.has(statement)) { + dfs(statement); + } + }); + + return stack; +} diff --git a/packages/compiler/src/utils/tests/get-referenced-variables-inside.test.ts b/packages/compiler/src/utils/tests/get-referenced-variables-inside.test.ts index 92c3690..822b42b 100644 --- a/packages/compiler/src/utils/tests/get-referenced-variables-inside.test.ts +++ b/packages/compiler/src/utils/tests/get-referenced-variables-inside.test.ts @@ -38,7 +38,8 @@ describe("getReferencedVariablesInside", () => { const fn = getFunctionFromBodyPath(body); const returnStatement = getReturnStatement(fn)!; - const bindings = getReferencedVariablesInside(returnStatement); + const variables = getReferencedVariablesInside(returnStatement); + const bindings = Array.from(variables.values()); expect(bindings).toHaveLength(1); expect(bindings[0]?.path).toEqual(body.at(0)?.get("declarations.0")); @@ -55,7 +56,8 @@ describe("getReferencedVariablesInside", () => { const fn = getFunctionFromBodyPath(body); const returnStatement = getReturnStatement(fn)!; - const bindings = getReferencedVariablesInside(returnStatement); + const variables = getReferencedVariablesInside(returnStatement); + const bindings = Array.from(variables.values()); expect(bindings).toHaveLength(1); expect(bindings[0]?.identifier.name).toStrictEqual("derivedValue"); @@ -75,7 +77,8 @@ describe("getReferencedVariablesInside", () => { const fn = getFunctionFromBodyPath(body); const returnStatement = getReturnStatement(fn)!; - const bindings = getReferencedVariablesInside(returnStatement); + const variables = getReferencedVariablesInside(returnStatement); + const bindings = Array.from(variables.values()); expect(bindings).toHaveLength(2); expect(bindings[0]?.identifier.name).toStrictEqual("derivedValue"); @@ -97,9 +100,9 @@ function MyComponent() { `); const fn = getFunctionFromBodyPath(body); const returnStatement = getReturnStatement(fn)!; - const bindings = getReferencedVariablesInside(returnStatement); + const variables = getReferencedVariablesInside(returnStatement); + const bindings = Array.from(variables.values()); - // expect(bindings).toHaveLength(3); expect(bindings.map((b) => b.identifier.name)).toStrictEqual([ "obj", "key", @@ -118,7 +121,8 @@ function MyComponent() { `); const fn = getFunctionFromBodyPath(body); const returnStatement = getReturnStatement(fn)!; - const bindings = getReferencedVariablesInside(returnStatement); + const variables = getReferencedVariablesInside(returnStatement); + const bindings = Array.from(variables.values()); expect(bindings).toHaveLength(3); expect(bindings.map((b) => b.identifier.name)).toStrictEqual([ @@ -139,7 +143,8 @@ function MyComponent() { `); const fn = getFunctionFromBodyPath(body); const returnStatement = getReturnStatement(fn)!; - const bindings = getReferencedVariablesInside(returnStatement); + const variables = getReferencedVariablesInside(returnStatement); + const bindings = Array.from(variables.values()); expect(bindings).toHaveLength(3); expect(bindings.map((b) => b.identifier.name)).toStrictEqual([ @@ -158,7 +163,8 @@ function MyComponent() { `); const fn = getFunctionFromBodyPath(body); - const bindings = getReferencedVariablesInside(fn); + const variables = getReferencedVariablesInside(fn); + const bindings = Array.from(variables.values()); expect(bindings).toHaveLength(0); }); @@ -173,7 +179,8 @@ function MyComponent() { `); const fn = getFunctionFromBodyPath(body); - const bindings = getReferencedVariablesInside(fn); + const variables = getReferencedVariablesInside(fn); + const bindings = Array.from(variables.values()); expect(bindings).toHaveLength(0); }); @@ -196,7 +203,8 @@ function MyComponent() { `); const fn = getFunctionFromBodyPath(body); - const bindings = getReferencedVariablesInside(fn); + const variables = getReferencedVariablesInside(fn); + const bindings = Array.from(variables.values()); expect(bindings.map((b) => b.identifier.name)).toStrictEqual([ "computedProperty", @@ -224,7 +232,8 @@ function MyComponent() { `); const fn = getFunctionFromBodyPath(body); - const bindings = getReferencedVariablesInside(fn); + const variables = getReferencedVariablesInside(fn); + const bindings = Array.from(variables.values()); expect(bindings.map((b) => b.identifier.name)).toStrictEqual([ "computedProperty", diff --git a/packages/compiler/src/utils/tests/get-rightmost-id-name.test.ts b/packages/compiler/src/utils/tests/get-rightmost-id-name.test.ts index c28563e..1c367d4 100644 --- a/packages/compiler/src/utils/tests/get-rightmost-id-name.test.ts +++ b/packages/compiler/src/utils/tests/get-rightmost-id-name.test.ts @@ -6,9 +6,11 @@ import { parse } from "../testing"; const parseCodeAndRun = (code: string) => { const path = parse(code); return getRightmostIdName( - path.get( - "body.0.declarations.0.init" - ) as babel.NodePath + ( + path.get( + "body.0.declarations.0.init" + ) as babel.NodePath + ).node ); }; diff --git a/packages/compiler/src/utils/tests/is-in-the-same-function-scope.test.ts b/packages/compiler/src/utils/tests/is-in-the-same-function-scope.test.ts new file mode 100644 index 0000000..50e6819 --- /dev/null +++ b/packages/compiler/src/utils/tests/is-in-the-same-function-scope.test.ts @@ -0,0 +1,76 @@ +import * as babel from "@babel/core"; +import { isInTheSameFunctionScope } from "../is-in-the-same-function-scope"; +import { parse } from "../testing"; + +describe("isInTheSameFunctionScope", () => { + it("basic example", () => { + const path = parse(` + function testFunction() { + return true; + } + `); + + const testFn = path.get("body.0") as babel.NodePath; + const returnStatement = testFn.get( + "body.body.0" + ) as babel.NodePath; + + expect(isInTheSameFunctionScope(returnStatement, testFn)).toStrictEqual( + true + ); + }); + + it("with node in an if statement block", () => { + const path = parse(` +function testFunction() { + if (true) { + return true; + } +} + `); + + const testFn = path.get("body.0") as babel.NodePath; + const returnStatement = testFn.get( + "body.body.0.consequent.body.0" + ) as babel.NodePath; + + expect(isInTheSameFunctionScope(returnStatement, testFn)).toStrictEqual( + true + ); + }); + + it("detects when a path is not in the same function scope immediately", () => { + const path = parse(` +function testFunction() { + const callback = () => { + return 'foo'; + } + if (true) { + return true; + } +} + `); + + const testFn = path.get("body.0") as babel.NodePath; + const callbackFn = testFn.get( + "body.body.0.declarations.0.init" + ) as babel.NodePath; + const returnStatementInCallback = callbackFn.get( + "body.body.0" + ) as babel.NodePath; + + const returnStatement = testFn.get( + "body.body.1.consequent.body.0" + ) as babel.NodePath; + + expect(isInTheSameFunctionScope(returnStatement, testFn)).toStrictEqual( + true + ); + expect( + isInTheSameFunctionScope(returnStatementInCallback, testFn) + ).toStrictEqual(false); + expect( + isInTheSameFunctionScope(returnStatementInCallback, callbackFn) + ).toStrictEqual(true); + }); +}); diff --git a/packages/compiler/src/utils/tests/member-expression-to-dot-notation.test.ts b/packages/compiler/src/utils/tests/member-expression-to-dot-notation.test.ts deleted file mode 100644 index 8857fca..0000000 --- a/packages/compiler/src/utils/tests/member-expression-to-dot-notation.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as babel from "@babel/core"; -import { memberExpressionToDotNotation } from "../member-expression-to-dot-notation"; -import { parse } from "../testing"; - -function parseExpression(code: string) { - const ast = parse(code); - return ( - ast.get("body.0.expression") as babel.NodePath - ).node; -} - -describe("memberExpressionToDotNotation", () => { - it("should handle Identifier", () => { - const node = parseExpression("foo.bar"); - expect(memberExpressionToDotNotation(node)).toBe("foo.bar"); - }); - - it("should handle Literal", () => { - const node = parseExpression("foo[123]"); - expect(memberExpressionToDotNotation(node)).toBe("foo[123]"); - }); - - it("should handle nested MemberExpression", () => { - const node = parseExpression("foo.bar.baz"); - expect(memberExpressionToDotNotation(node)).toBe("foo.bar.baz"); - }); - - it("should handle computed MemberExpression", () => { - const node = parseExpression("foo[bar.baz]"); - expect(memberExpressionToDotNotation(node)).toBe("foo[bar.baz]"); - }); - - it("should handle ThisExpression", () => { - const node = parseExpression("this.foo"); - expect(memberExpressionToDotNotation(node)).toBe("this.foo"); - }); - - it("should handle computed ThisExpression", () => { - const node = parseExpression("this[foo]"); - expect(memberExpressionToDotNotation(node)).toBe("this[foo]"); - }); - - it("should handle computed string literal", () => { - const node = parseExpression("test['foo']"); - expect(memberExpressionToDotNotation(node)).toBe('test["foo"]'); - }); - - it("should handle computed string template", () => { - const node = parseExpression("test[`foo`]"); - expect(memberExpressionToDotNotation(node)).toBe("test[`foo`]"); - }); - - it("should handle bigint literal", () => { - const node = parseExpression("test[123n]"); - expect(memberExpressionToDotNotation(node)).toBe("test[123n]"); - }); -}); diff --git a/packages/compiler/src/utils/unwrap-jsx-elements.ts b/packages/compiler/src/utils/unwrap-jsx-elements.ts index 024557e..42d7f98 100644 --- a/packages/compiler/src/utils/unwrap-jsx-elements.ts +++ b/packages/compiler/src/utils/unwrap-jsx-elements.ts @@ -1,6 +1,7 @@ import * as babel from "@babel/core"; import * as t from "@babel/types"; import { DEFAULT_UNWRAPPED_JSX_ELEMENT_VARIABLE_NAME } from "./constants"; +import { isInTheSameFunctionScope } from "./is-in-the-same-function-scope"; import { unwrapGenericExpression } from "./unwrap-generic-expression"; type JSXChild = t.JSXElement["children"][number]; @@ -23,6 +24,9 @@ export function unwrapJsxElements(fn: babel.NodePath) { } if (path.isJSXElement() || path.isJSXFragment()) { + if (!isInTheSameFunctionScope(path, fn)) { + return; + } const childrenPath = path.get("children") as babel.NodePath< t.JSXElement["children"][number] >[]; diff --git a/packages/compiler/src/utils/unwrap-jsx-expressions.ts b/packages/compiler/src/utils/unwrap-jsx-expressions.ts index fb4e259..d784fad 100644 --- a/packages/compiler/src/utils/unwrap-jsx-expressions.ts +++ b/packages/compiler/src/utils/unwrap-jsx-expressions.ts @@ -1,6 +1,7 @@ import * as babel from "@babel/core"; import * as t from "@babel/types"; import { DEFAULT_UNWRAPPED_JSX_EXPRESSION_VARIABLE_NAME } from "./constants"; +import { isInTheSameFunctionScope } from "./is-in-the-same-function-scope"; import { unwrapGenericExpression } from "./unwrap-generic-expression"; export function unwrapJsxExpressions(fn: babel.NodePath) { @@ -16,6 +17,9 @@ export function unwrapJsxExpressions(fn: babel.NodePath) { ) { return; } + if (!isInTheSameFunctionScope(path, fn)) { + return; + } unwrapGenericExpression( fn, diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 17f3f46..956ff99 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -22,7 +22,7 @@ interface Commit { export const useCreateCache$unforget = ( size: S -): [FixedArray, Commit] => { +): [FixedArray, Commit, UnassignedType] => { const valuesRef: MutableRefObject | null> = useRef(null); @@ -31,6 +31,9 @@ export const useCreateCache$unforget = ( const valuesToCommit: MutableRefObject | null> = useRef(null); + // This is needed for hot reloading to work + const previousSize = useRef(size); + if (!valuesToCommit.current) { valuesToCommit.current = new Map(); } @@ -46,7 +49,8 @@ export const useCreateCache$unforget = ( }; } - if (!valuesRef.current) { + if (!valuesRef.current || previousSize.current !== size) { + previousSize.current = size; valuesRef.current = Array.from({ length: size }, (_, i) => { return { v: UNASSIGNED, @@ -64,5 +68,5 @@ export const useCreateCache$unforget = ( throw new Error("Unreachable"); } - return [valuesRef.current, commitRef.current]; + return [valuesRef.current, commitRef.current, UNASSIGNED]; };