feat: viewNode as child

This commit is contained in:
Hoikan 2024-04-25 15:58:26 +08:00
parent f15b7d1a14
commit be4456f225
11 changed files with 186 additions and 243 deletions

View File

@ -0,0 +1,78 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { NodePath } from '@babel/core';
import { LifeCycle, Visitor } from './types';
import { addLifecycle, addWatch } from './nodeFactory';
import * as t from '@babel/types';
import { ON_MOUNT, ON_UNMOUNT, WATCH, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
import { extractFnFromMacro, getFnBody } from '../utils';
function isLifeCycleName(name: string): name is LifeCycle {
return [WILL_MOUNT, ON_MOUNT, WILL_UNMOUNT, ON_UNMOUNT].includes(name);
}
/**
* Analyze the functional macro in the function component
* 1. lifecycle
* 1. willMount
* 2. onMount
* 3. willUnMount
* 4. onUnmount
* 2. watch
*/
export function functionalMacroAnalyze(): Visitor {
return {
ExpressionStatement(path: NodePath<t.ExpressionStatement>, ctx) {
const expression = path.get('expression');
if (expression.isCallExpression()) {
const callee = expression.get('callee');
if (callee.isIdentifier()) {
const calleeName = callee.node.name;
// lifecycle
if (isLifeCycleName(calleeName)) {
const fnNode = extractFnFromMacro(expression, calleeName);
addLifecycle(ctx.current, calleeName, getFnBody(fnNode).node);
return;
}
// watch
if (calleeName === WATCH) {
const fnNode = extractFnFromMacro(expression, WATCH);
const deps = getWatchDeps(expression);
addWatch(ctx.current, fnNode, deps);
return;
}
}
}
ctx.unhandledNode.push(path.node);
},
};
}
function getWatchDeps(callExpression: NodePath<t.CallExpression>) {
const args = callExpression.get('arguments');
if (!args[1]) {
return null;
}
let deps: null | NodePath<t.ArrayExpression> = null;
if (args[1].isArrayExpression()) {
deps = args[1];
} else {
console.error('watch deps should be an array expression');
}
return deps;
}

View File

@ -1,11 +1,14 @@
import { type types as t, type NodePath } from '@babel/core';
import { propsAnalyze } from './propsAnalyze';
import { AnalyzeContext, Analyzer, ComponentNode, CondNode, Visitor } from './types';
import { createComponentNode } from './nodeFactory';
import { addLifecycle, createComponentNode } from './nodeFactory';
import { propertiesAnalyze } from './propertiesAnalyze';
import { lifeCycleAnalyze } from './lifeCycleAnalyze';
import { functionalMacroAnalyze } from './functionalMacroAnalyze';
import { getFnBody } from '../utils';
const builtinAnalyzers = [propsAnalyze, propertiesAnalyze, lifeCycleAnalyze];
import { viewAnalyze } from './viewAnalyze';
import { WILL_MOUNT } from '../constants';
import { types } from '../babelTypes';
const builtinAnalyzers = [propsAnalyze, propertiesAnalyze, functionalMacroAnalyze, viewAnalyze];
export function isCondNode(node: any): node is CondNode {
return node && node.type === 'cond';
@ -13,22 +16,7 @@ export function isCondNode(node: any): node is CondNode {
function mergeVisitor(...visitors: Analyzer[]): Visitor {
return visitors.reduce<Visitor<AnalyzeContext>>((acc, cur) => {
const visitor = cur();
const visitorKeys = Object.keys(visitor) as (keyof Visitor)[];
for (const key of visitorKeys) {
if (acc[key]) {
// if already exist, merge the visitor function
const original = acc[key]!;
acc[key] = (path: any, ctx) => {
original(path, ctx);
visitor[key]?.(path, ctx);
};
} else {
// @ts-expect-error key is a valid key, no idea why it's not working
acc[key] = visitor[key];
}
}
return acc;
return { ...acc, ...cur() };
}, {});
}
@ -45,6 +33,7 @@ export function analyzeFnComp(
current: componentNode,
htmlTags,
analyzers,
unhandledNode: [],
traverse: (path: NodePath<t.Statement>, ctx: AnalyzeContext) => {
path.traverse(visitor, ctx);
},
@ -71,14 +60,23 @@ export function analyzeFnComp(
const type = p.node.type;
// TODO: More type safe way to handle this
visitor[type]?.(p as unknown as any, context);
const visit = visitor[type];
if (visit) {
// TODO: More type safe way to handle this
visit(p as unknown as any, context);
} else {
context.unhandledNode.push(p.node);
}
if (p.isReturnStatement()) {
visitor.ReturnStatement?.(p, context);
break;
}
}
if (context.unhandledNode.length) {
addLifecycle(componentNode, WILL_MOUNT, types.blockStatement(context.unhandledNode));
}
}
/**
* The process of analyzing the component

View File

@ -1,49 +0,0 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { NodePath } from '@babel/core';
import { LifeCycle, Visitor } from './types';
import { addLifecycle } from './nodeFactory';
import * as t from '@babel/types';
import { ON_MOUNT, ON_UNMOUNT, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
import { extractFnFromMacro, getFnBody } from '../utils';
function isLifeCycleName(name: string): name is LifeCycle {
return [WILL_MOUNT, ON_MOUNT, WILL_UNMOUNT, ON_UNMOUNT].includes(name);
}
/**
* Analyze the lifeCycle in the function component
* 1. willMount
* 2. onMount
* 3. willUnMount
* 4. onUnmount
*/
export function lifeCycleAnalyze(): Visitor {
return {
ExpressionStatement(path: NodePath<t.ExpressionStatement>, ctx) {
const expression = path.get('expression');
if (expression.isCallExpression()) {
const callee = expression.get('callee');
if (callee.isIdentifier()) {
const lifeCycleName = callee.node.name;
if (isLifeCycleName(lifeCycleName)) {
const fnNode = extractFnFromMacro(expression, lifeCycleName);
addLifecycle(ctx.current, lifeCycleName, getFnBody(fnNode));
}
}
}
},
};
}

View File

@ -14,12 +14,13 @@
*/
import { NodePath, type types as t } from '@babel/core';
import { Branch, ComponentNode, CondNode, InulaNode, JSX, JSXNode, LifeCycle, SubCompNode } from './types';
import { ComponentNode, FunctionalExpression, LifeCycle, ViewNode } from './types';
import { PropType } from '../constants';
import { ViewParticle } from '@openinula/reactivity-parser';
export function createComponentNode(
name: string,
fnNode: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,
fnNode: NodePath<FunctionalExpression>,
parent?: ComponentNode
): ComponentNode {
const comp: ComponentNode = {
@ -27,12 +28,12 @@ export function createComponentNode(
name,
props: [],
child: undefined,
properties: [],
variables: [],
dependencyMap: {},
reactiveMap: {},
lifecycle: {},
parent,
// fnBody,
fnNode,
get availableProps() {
return comp.props
.map(({ name, nestedProps, alias }) => {
@ -41,11 +42,11 @@ export function createComponentNode(
})
.flat();
},
get ownAvailableProperties() {
return [...comp.properties.filter(p => !p.isMethod).map(({ name }) => name), ...comp.availableProps];
get ownAvailableVariables() {
return [...comp.variables.filter(p => p.type === 'reactive').map(({ name }) => name), ...comp.availableProps];
},
get availableProperties() {
return [...comp.ownAvailableProperties, ...(comp.parent ? comp.parent.availableProperties : [])];
get availableVariables() {
return [...comp.ownAvailableVariables, ...(comp.parent ? comp.parent.availableVariables : [])];
},
};
@ -53,15 +54,15 @@ export function createComponentNode(
}
export function addProperty(comp: ComponentNode, name: string, value: t.Expression | null, isComputed: boolean) {
comp.properties.push({ name, value, isComputed, isMethod: false });
comp.variables.push({ name, value, isComputed, type: 'reactive' });
}
export function addMethod(comp: ComponentNode, name: string, value: t.Expression | null) {
comp.properties.push({ name, value, isComputed: false, isMethod: true });
export function addMethod(comp: ComponentNode, name: string, value: FunctionalExpression) {
comp.variables.push({ name, value, type: 'method' });
}
export function addSubComponent(comp: ComponentNode, subComp: ComponentNode, isComputed: boolean) {
comp.properties.push({ name: subComp.name, value: subComp, isSubComp: true, isComputed, isMethod: false });
export function addSubComponent(comp: ComponentNode, subComp: ComponentNode) {
comp.variables.push({ name: subComp.name, value: subComp, type: 'subComp' });
}
export function addProp(
@ -76,7 +77,7 @@ export function addProp(
comp.props.push({ name: key, type, default: defaultVal, alias, nestedProps, nestedRelationship });
}
export function addLifecycle(comp: ComponentNode, lifeCycle: LifeCycle, block: NodePath<t.BlockStatement>) {
export function addLifecycle(comp: ComponentNode, lifeCycle: LifeCycle, block: t.BlockStatement) {
const compLifecycle = comp.lifecycle;
if (!compLifecycle[lifeCycle]) {
compLifecycle[lifeCycle] = [];
@ -96,28 +97,10 @@ export function addWatch(
comp.watch.push({ callback, deps });
}
export function createJSXNode(parent: ComponentNode, content: NodePath<JSX>): JSXNode {
return {
type: 'jsx',
parent,
child: content,
};
}
export function createCondNode(parent: ComponentNode, child: InulaNode, branches: Branch[]): CondNode {
return {
type: 'cond',
branches,
child,
parent,
};
}
export function createSubCompNode(name: string, parent: ComponentNode, child: JSX): SubCompNode {
return {
type: 'subComp',
name,
parent,
child,
export function setViewChild(comp: ComponentNode, view: ViewParticle[], usedPropertySet: Set<string>) {
const viewNode: ViewNode = {
content: view,
usedPropertySet,
};
comp.child = viewNode;
}

View File

@ -14,7 +14,7 @@
*/
import { AnalyzeContext, Visitor } from './types';
import { addMethod, addProperty, createComponentNode } from './nodeFactory';
import { addMethod, addProperty, addSubComponent, createComponentNode } from './nodeFactory';
import { isValidPath } from './utils';
import { type types as t, type NodePath } from '@babel/core';
import { reactivityFuncNames } from '../const';
@ -45,11 +45,12 @@ export function propertiesAnalyze(): Visitor {
const init = declaration.get('init');
let deps: string[] | null = null;
if (isValidPath(init)) {
// the property is a method
// handle the method
if (init.isArrowFunctionExpression() || init.isFunctionExpression()) {
addMethod(ctx.current, id.node.name, init.node);
return;
}
// handle the sub component
// Should like Component(() => {})
if (
init.isCallExpression() &&
@ -64,9 +65,10 @@ export function propertiesAnalyze(): Visitor {
analyzeFnComp(fnNode, subComponent, ctx);
deps = getDependenciesFromNode(id.node.name, init, ctx);
addProperty(ctx.current, id.node.name, subComponent, !!deps?.length);
addSubComponent(ctx.current, subComponent);
return;
}
deps = getDependenciesFromNode(id.node.name, init, ctx);
}
addProperty(ctx.current, id.node.name, init.node || null, !!deps?.length);
@ -111,7 +113,7 @@ function getDependenciesFromNode(
const propertyKey = innerPath.node.name;
if (isAssignmentExpressionLeft(innerPath) || isAssignmentFunction(innerPath)) {
assignDeps.add(propertyKey);
} else if (current.availableProperties.includes(propertyKey)) {
} else if (current.availableVariables.includes(propertyKey)) {
deps.add(propertyKey);
}
};

View File

@ -14,32 +14,36 @@
*/
import { type NodePath, types as t } from '@babel/core';
import { Node } from '@babel/traverse';
import { ON_MOUNT, ON_UNMOUNT, PropType, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
import { ViewParticle } from '@openinula/reactivity-parser';
// --- Node shape ---
export type InulaNode = ComponentNode | CondNode | JSXNode;
export type JSX = t.JSXElement | t.JSXFragment;
export type LifeCycle = typeof WILL_MOUNT | typeof ON_MOUNT | typeof WILL_UNMOUNT | typeof ON_UNMOUNT;
type defaultVal = any | null;
type Bitmap = number;
interface BaseProperty<V> {
export type FunctionalExpression = t.FunctionExpression | t.ArrowFunctionExpression;
interface BaseVariable<V> {
name: string;
value: V;
// need a flag for computed to gen a getter
// watch is a static computed
isComputed: boolean;
isMethod: boolean;
}
interface Property extends BaseProperty<t.Expression | null> {
export interface ReactiveVariable extends BaseVariable<t.Expression | null> {
type: 'reactive';
// indicate the value is a state or computed or watch
listeners?: string[];
bitmap?: Bitmap;
// need a flag for computed to gen a getter
// watch is a static computed
isComputed: boolean;
}
interface SubCompProperty extends BaseProperty<ComponentNode> {
isSubComp: true;
export interface MethodVariable extends BaseVariable<FunctionalExpression> {
type: 'method';
}
interface Prop {
export interface SubCompVariable extends BaseVariable<ComponentNode> {
type: 'subComp';
}
export type Variable = ReactiveVariable | MethodVariable | SubCompVariable;
export interface Prop {
name: string;
type: PropType;
alias: string | null;
@ -51,66 +55,45 @@ export interface ComponentNode {
type: 'comp';
name: string;
props: Prop[];
// A properties could be a state or computed
properties: (Property | SubCompProperty)[];
// The variables defined in the component
variables: Variable[];
/**
* The available props for the component, including the nested props
*/
availableProps: string[];
/**
* The available properties for the component
* The available variables and props owned by the component
*/
ownAvailableProperties: string[];
availableProperties: string[];
ownAvailableVariables: string[];
/**
* The available variables and props for the component and its parent
*/
availableVariables: string[];
/**
* The map to find the dependencies
*/
dependencyMap: {
[key: string]: string[];
};
child?: InulaNode;
child?: ComponentNode | ViewNode;
parent?: ComponentNode;
/**
* The function body of the fn component code
*/
fnBody: NodePath<t.Statement>[];
fnNode: NodePath<FunctionalExpression>;
/**
* The map to find the state
*/
reactiveMap: Record<string, Bitmap>;
lifecycle: Partial<Record<LifeCycle, NodePath<t.Statement>[]>>;
lifecycle: Partial<Record<LifeCycle, t.Statement[]>>;
watch?: {
deps: NodePath<t.ArrayExpression> | null;
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>;
}[];
}
export interface SubCompNode {
type: 'subComp';
name: string;
parent: ComponentNode;
child: JSX;
}
export interface JSXNode {
type: 'jsx';
parent: ComponentNode;
child: NodePath<JSX>;
}
export interface CondNode {
type: 'cond';
branches: Branch[];
parent: ComponentNode;
/**
* The default branch
*/
child: InulaNode;
}
export interface Branch {
conditions: NodePath<t.Expression>[];
content: InulaNode;
export interface ViewNode {
content: ViewParticle[];
usedPropertySet: Set<string>;
}
export interface AnalyzeContext {
@ -119,10 +102,11 @@ export interface AnalyzeContext {
analyzers: Analyzer[];
htmlTags: string[];
traverse: (p: NodePath<t.Statement>, ctx: AnalyzeContext) => void;
unhandledNode: t.Statement[];
}
export type Visitor<S = AnalyzeContext> = {
[Type in Node['type']]?: (path: NodePath<Extract<Node, { type: Type }>>, state: S) => void;
[Type in t.Statement['type']]?: (path: NodePath<Extract<t.Statement, { type: Type }>>, state: S) => void;
} & {
Prop?: (path: NodePath<t.ObjectProperty | t.RestElement>, state: S) => void;
};

View File

@ -19,6 +19,7 @@ import { parseView as parseJSX } from 'jsx-view-parser';
import { getBabelApi } from '../babelTypes';
import { parseReactivity } from '@openinula/reactivity-parser';
import { reactivityFuncNames } from '../const';
import { setViewChild } from './nodeFactory';
/**
* Analyze the watch in the function component
@ -35,10 +36,12 @@ export function viewAnalyze(): Visitor {
});
const [viewParticles, usedPropertySet] = parseReactivity(viewUnits, {
babelApi: getBabelApi(),
availableProperties: current.availableProperties,
availableProperties: current.availableVariables,
dependencyMap: current.dependencyMap,
reactivityFuncNames,
});
setViewChild(current, viewParticles, usedPropertySet);
}
},
};

View File

@ -1,55 +0,0 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { NodePath } from '@babel/core';
import { Visitor } from './types';
import { addWatch } from './nodeFactory';
import * as t from '@babel/types';
import { WATCH } from '../constants';
import { extractFnFromMacro } from '../utils';
/**
* Analyze the watch in the function component
*/
export function watchAnalyze(): Visitor {
return {
ExpressionStatement(path: NodePath<t.ExpressionStatement>, ctx) {
const callExpression = path.get('expression');
if (callExpression.isCallExpression()) {
const callee = callExpression.get('callee');
if (callee.isIdentifier() && callee.node.name === WATCH) {
const fnNode = extractFnFromMacro(callExpression, WATCH);
const deps = getWatchDeps(callExpression);
addWatch(ctx.current, fnNode, deps);
}
}
},
};
}
function getWatchDeps(callExpression: NodePath<t.CallExpression>) {
const args = callExpression.get('arguments');
if (!args[1]) {
return null;
}
let deps: null | NodePath<t.ArrayExpression> = null;
if (args[1].isArrayExpression()) {
deps = args[1];
} else {
console.error('watch deps should be an array expression');
}
return deps;
}

View File

@ -15,12 +15,12 @@
import { describe, expect, it } from 'vitest';
import { genCode, mockAnalyze } from '../mock';
import { lifeCycleAnalyze } from '../../src/analyze/lifeCycleAnalyze';
import { functionalMacroAnalyze } from '../../src/analyze/functionalMacroAnalyze';
import { types } from '../../src/babelTypes';
import { type NodePath, type types as t } from '@babel/core';
const analyze = (code: string) => mockAnalyze(code, [lifeCycleAnalyze]);
const combine = (body: NodePath<t.Statement>[]) => types.program(body.map(path => path.node));
const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze]);
const combine = (body: t.Statement[]) => types.program(body);
describe('analyze lifeCycle', () => {
it('should collect will mount', () => {

View File

@ -29,7 +29,7 @@ describe('analyze properties', () => {
let bar = 1;
})
`);
expect(root.properties.length).toBe(2);
expect(root.variables.length).toBe(2);
});
describe('state dependency', () => {
@ -40,11 +40,11 @@ describe('analyze properties', () => {
let bar = foo;
})
`);
expect(root.properties.length).toBe(2);
expect(root.properties[0].isComputed).toBe(false);
expect(genCode(root.properties[0].value)).toBe('1');
expect(root.properties[1].isComputed).toBe(true);
expect(genCode(root.properties[1].value)).toBe('foo');
expect(root.variables.length).toBe(2);
expect(root.variables[0].isComputed).toBe(false);
expect(genCode(root.variables[0].value)).toBe('1');
expect(root.variables[1].isComputed).toBe(true);
expect(genCode(root.variables[1].value)).toBe('foo');
expect(root.dependencyMap).toEqual({ bar: ['foo'] });
});
@ -57,9 +57,9 @@ describe('analyze properties', () => {
let bar = { foo: foo ? a : b };
})
`);
expect(root.properties.length).toBe(4);
expect(root.properties[3].isComputed).toBe(true);
expect(genCode(root.properties[3].value)).toMatchInlineSnapshot(`
expect(root.variables.length).toBe(4);
expect(root.variables[3].isComputed).toBe(true);
expect(genCode(root.variables[3].value)).toMatchInlineSnapshot(`
"{
foo: foo ? a : b
}"
@ -73,8 +73,8 @@ describe('analyze properties', () => {
let bar = foo;
})
`);
expect(root.properties.length).toBe(1);
expect(root.properties[0].isComputed).toBe(true);
expect(root.variables.length).toBe(1);
expect(root.variables[0].isComputed).toBe(true);
expect(root.dependencyMap).toEqual({ bar: ['foo'] });
});
@ -84,8 +84,8 @@ describe('analyze properties', () => {
let bar = [foo1, first, last];
})
`);
expect(root.properties.length).toBe(1);
expect(root.properties[0].isComputed).toBe(true);
expect(root.variables.length).toBe(1);
expect(root.variables[0].isComputed).toBe(true);
expect(root.dependencyMap).toEqual({ bar: ['foo1', 'first', 'last'] });
});
@ -96,8 +96,8 @@ describe('analyze properties', () => {
let bar = cond ? count : window.innerWidth;
})
`);
expect(root.properties.length).toBe(1);
expect(root.properties[0].isComputed).toBe(false);
expect(root.variables.length).toBe(1);
expect(root.variables[0].isComputed).toBe(false);
expect(root.dependencyMap).toEqual({});
});
});
@ -112,9 +112,9 @@ describe('analyze properties', () => {
});
})
`);
expect(root.properties.length).toBe(2);
expect(root.variables.length).toBe(2);
expect(root.dependencyMap).toEqual({ Sub: ['foo'] });
expect((root.properties[1].value as ComponentNode).dependencyMap).toMatchInlineSnapshot(`
expect((root.variables[1].value as ComponentNode).dependencyMap).toMatchInlineSnapshot(`
{
"bar": [
"foo",
@ -135,9 +135,9 @@ describe('analyze properties', () => {
function onInput() {}
})
`);
expect(root.properties.map(p => p.name)).toEqual(['foo', 'onClick', 'onHover', 'onInput']);
expect(root.properties[1].isMethod).toBe(true);
expect(root.properties[2].isMethod).toBe(true);
expect(root.variables.map(p => p.name)).toEqual(['foo', 'onClick', 'onHover', 'onInput']);
expect(root.variables[1].type).toBe('method');
expect(root.variables[2].type).toBe('method');
expect(root.dependencyMap).toMatchInlineSnapshot('{}');
});
});

View File

@ -1,9 +1,8 @@
import { propsAnalyze } from '../../src/analyze/propsAnalyze';
import { watchAnalyze } from '../../src/analyze/watchAnalyze';
import { functionalMacroAnalyze } from '../../src/analyze/functionalMacroAnalyze';
import { genCode, mockAnalyze } from '../mock';
import { describe, expect, it } from 'vitest';
const analyze = (code: string) => mockAnalyze(code, [watchAnalyze]);
const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze]);
describe('watchAnalyze', () => {
it('should analyze watch expressions', () => {