-
Notifications
You must be signed in to change notification settings - Fork 7
/
functionScope.ts
184 lines (161 loc) · 6.37 KB
/
functionScope.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import { Context } from 'src/ts2php/components/context';
import { Declaration } from '../types';
import * as ts from 'typescript';
import { fetchAllBindingIdents, getClosestOrigParentOfType } from '../utils/ast';
import { handleComponent } from './react/reactComponents';
import { BoundNode } from './unusedCodeElimination/usageGraph/node';
import { renderNode, renderNodes } from './codegen/renderNodes';
import { usedInNestedScope } from './unusedCodeElimination/usageGraph/nodeData';
import { getTimeMarker } from '../utils/hrtime';
import { snakify } from '../utils/pathsAndNames';
type FunctionalDecl = ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction;
export function getRenderedBlock(
context: Context<Declaration>,
nodeIdent: string,
realParent: ts.Node | undefined,
argSynList: ts.NodeArray<ts.ParameterDeclaration>,
bodyBlock?: ts.Node // can be many types in arrow func
) {
let node;
const [, declScope] = context.scope.findByIdent(nodeIdent) || [];
if (declScope) {
node = (declScope.declarations.get(nodeIdent) as BoundNode<Declaration>);
}
if (!node) {
context.scope.addDeclaration(nodeIdent, [], { dryRun: context.dryRun });
}
const stackCtr = getTimeMarker();
context.pushScope(`function__${stackCtr}`, nodeIdent);
// Declare all parameters
argSynList.map(fetchAllBindingIdents)
.reduce((acc, val) => acc.concat(val), []) // flatten;
.forEach((ident) => {
context.scope.addDeclaration(
ident.getText(),
[],
{ terminateLocally: true, dryRun: context.dryRun }
);
});
if (realParent) {
context.nodeFlagsStore.upsert(realParent, { destructuringInfo: { vars: '' } });
}
const [...syntaxList] = renderNodes([...argSynList], context, false);
const block = renderNode(bodyBlock, context);
const idMap = new Map<string, boolean>();
context.scope.getClosure().forEach((decl, ident) => {
if ((decl.flags.External) && decl.propName === '*') {
return; // imported vars should not get into closure
}
if (!!(decl.flags.HoistedToModule)) {
return; // module scope vars also should not get into closure
}
idMap.set(ident, !!(decl.flags.ModifiedInLowerScope));
});
context.popScope(`function__${stackCtr}`, bodyBlock?.getLastToken());
return { syntaxList, block, idMap };
}
type GenParams = {
expr: ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction;
nodeIdent: string;
context: Context<Declaration>;
origDecl?: ts.VariableDeclaration;
origStatement?: ts.Expression;
};
export function generateFunctionElements({ expr, nodeIdent, context, origDecl, origStatement }: GenParams) {
if (origDecl && origStatement) {
const parentStmt = getClosestOrigParentOfType(origDecl, ts.SyntaxKind.VariableStatement);
if (parentStmt) {
const handledContent = handleComponent(context, origStatement);
if (handledContent) {
return null; // component is written to different file, so we should not output anything here
}
}
}
const params = expr.parameters;
const blockNode = expr.body;
let { syntaxList, block } = getRenderedBlock(
context, nodeIdent, origStatement,
params, blockNode
);
block = unwrapArrowBody(block, blockNode);
block = prependDestructuredParams(block, expr, context);
block = prependDefaultParams(block, expr, context);
return { syntaxList, block };
}
export function genClosure(idMap: Map<string, boolean>, context: Context<Declaration>, node: ts.Node) {
const closureUse: string[] = [];
idMap.forEach((modifiedInClosure, varName) => {
if (modifiedInClosure) {
context.log.error('Closure-scoped variable %s has been modified inside closure, this will not work on server side', [varName], context.log.ctx(node));
closureUse.push(`/* !! MODIFIED INSIDE !! */$${snakify(varName)}`);
} else {
closureUse.push(`$${snakify(varName)}`);
}
// Reset closure modification flag for all closure vars: they can be used in next closures without modification, and it's ok
const [decl] = context.scope.findByIdent(varName) || [];
if (decl) {
decl.flags = { ...decl.flags, ModifiedInLowerScope: false };
}
});
const closureExpr = closureUse.length > 0 ?
` use (${closureUse.join(', ')})` :
'';
return { closureExpr };
}
export function prependDestructuredParams(block: string, func: FunctionalDecl, context: Context<Declaration>) {
const flags = context.nodeFlagsStore.get(func);
if (!flags?.destructuringInfo?.vars) {
return block;
}
return block.replace(/^{/, '{\n' + flags.destructuringInfo.vars);
}
export function prependDefaultParams(block: string, func: FunctionalDecl, context: Context<Declaration>) {
const flags = context.nodeFlagsStore.get(func);
if (!flags?.optionalParamsWithDefaults) {
return block;
}
return block.replace(/^{/, '{\n' + flags.optionalParamsWithDefaults.join('\n'));
}
export function unwrapArrowBody(block: string, blockNode?: ts.Node, noReturn = false) {
if (blockNode?.kind !== ts.SyntaxKind.Block) {
return noReturn ? `{\n${block};\n}` : `{\nreturn ${block};\n}`;
}
return block;
}
type ExprGenOptions = {
synList: ts.NodeArray<ts.ParameterDeclaration>;
blockNode?: ts.Node;
};
export const functionExpressionGen = (node: FunctionalDecl, ident: string) => (opts: ExprGenOptions, context: Context<Declaration>) => {
let { syntaxList, block, idMap } = getRenderedBlock(
context, ident, node,
opts.synList, opts.blockNode
);
block = unwrapArrowBody(block, opts.blockNode);
block = prependDestructuredParams(block, node, context);
block = prependDefaultParams(block, node, context);
const { closureExpr } = genClosure(idMap, context, node);
return `/* ${ident} */ function (${syntaxList})${closureExpr} ${block}`;
};
/**
* If node value looks like modified in current context, add declaration flag.
* Return declaration.
* @param node
* @param context
* @return Declaration
*/
export function checkModificationInNestedScope(node: ts.Identifier | null, context: Context<Declaration>) {
if (!node) {
return null;
}
const nodeText = node.escapedText.toString();
const [decl, declScope] = context.scope.findByIdent(nodeText) || [];
if (decl && declScope) {
const modifiedInLowerScope = usedInNestedScope(decl, declScope, context.scope);
if (modifiedInLowerScope && decl) {
decl.flags = { ...decl.flags, ModifiedInLowerScope: true };
}
return decl;
}
return null;
}