Skip to content

Commit 05d4127

Browse files
authored
[INTERNAL] JSModuleAnalyzer: update language metadata (#44)
The JSModuleAnalyzer internally uses some metadata that describes for each ESTree node type, which properties represent conditionally executed code branches and which properties represent unconditionally executed code branches. The metadata is used while visiting an AST to classify dependencies as 'static' ord 'conditional'. During the migration of the analyzer from Java to JavaScript, metadata was only maintained for a rudimentary set of nodes (basically ES5) and all other nodes have been marked as 'toBeDone'. This change closes this gap and maintaines metadata for all ES6 node types. Only the node types planned for ES7 are kept as 'toBeDone'.
1 parent 712dcff commit 05d4127

File tree

3 files changed

+154
-25
lines changed

3 files changed

+154
-25
lines changed

lib/lbt/analyzer/JSModuleAnalyzer.js

Lines changed: 89 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const UI5ClientConstants = require("../UI5ClientConstants");
99
const {findOwnProperty, getLocation, getPropertyKey, isMethodCall, isString} = require("../utils/ASTUtils");
1010
const log = require("@ui5/logger").getLogger("lbt:analyzer:JSModuleAnalyzer");
1111

12-
// --------------------------------------------------------------------------------------------------------------------
12+
// ------------------------------------------------------------------------------------------------------------------
1313

1414
const EnrichedVisitorKeys = (function() {
1515
const VisitorKeys = require("estraverse").VisitorKeys;
@@ -24,17 +24,44 @@ const EnrichedVisitorKeys = (function() {
2424
* E.g. in an IfExpression, the 'test' is always executed, whereas 'consequent'
2525
* and 'alternate' are only executed under certain conditions.
2626
*
27-
* The object is checked against the 'official' list of node types
28-
* and node keys as defined by 'estraverse' This helps to ensure that no new
29-
* syntax addition is missed and that the configured keys are valid.
27+
* While visiting the AST of a JavaSCript file, the JSModuleAnalyzer uses this information
28+
* to decide whether a code block is executed conditionally or unconditionally.
29+
* Besides this information which is inherent to the language, the analyzer uses
30+
* additional knowledge about special APIS / constructs (e.g. the factory function of
31+
* an AMD module is known to be executed when the module is executed, an IIFE is known to
32+
* be executed etc.)
33+
*
34+
* To be more robust against the evolution of the language, the object below is checked
35+
* against the 'official' list of node types and node keys as defined by 'estraverse'.
36+
* This helps to ensure that no new syntax addition is missed and that the configured
37+
* keys are valid.
3038
*/
3139
const TempKeys = {
3240
AssignmentExpression: [],
33-
AssignmentPattern: toBeDone(["left", "right"]),
41+
/*
42+
* function( >>>a=3<<<, b) {...}
43+
* var [>>>a=3<<<, b] = [...];
44+
*
45+
* The default value expression (right) is only evaluated when there's no other value in
46+
* the context of the pattern (e.g. destructuring or function call don't provide a value),
47+
* so it's a conditional branch.
48+
*/
49+
AssignmentPattern: ["right"],
3450
ArrayExpression: [],
35-
ArrayPattern: toBeDone(["elements"]),
51+
/*
52+
* var >>>[a=3, b]<<< = [...];
53+
* All elements in an array pattern are unconditional.
54+
*/
55+
ArrayPattern: [], // elements
56+
/*
57+
* The body of an arrow function is only executed when the arrow function is executed
58+
*/
3659
ArrowFunctionExpression: ["body"],
37-
AwaitExpression: toBeDone(["argument"]), // CAUTION: It's deferred to ES7.
60+
/*
61+
* The argument of await is always executed
62+
* TODO how to handle code after the await expression?
63+
*/
64+
AwaitExpression: [], // argument
3865
BlockStatement: [],
3966
BinaryExpression: [],
4067
BreakStatement: [],
@@ -49,12 +76,16 @@ const EnrichedVisitorKeys = (function() {
4976
ContinueStatement: [],
5077
DebuggerStatement: [],
5178
DirectiveStatement: [],
52-
DoWhileStatement: [], // condition is executed on the same conditions as the surrounding block, potentially repeated, block is always entered and might be repeated
79+
/*
80+
* 'condition' is executed on the same conditions as the surrounding block, potentially repeated,
81+
* 'block' is always entered and might be repeated
82+
*/
83+
DoWhileStatement: [],
5384
EmptyStatement: [],
54-
ExportAllDeclaration: toBeDone(["source"]),
55-
ExportDefaultDeclaration: toBeDone(["declaration"]),
56-
ExportNamedDeclaration: toBeDone(["declaration", "specifiers", "source"]),
57-
ExportSpecifier: toBeDone(["exported", "local"]),
85+
ExportAllDeclaration: [], // no parts of an export are conditional - source
86+
ExportDefaultDeclaration: [], // no parts of an export are conditional - declaration
87+
ExportNamedDeclaration: [], // no parts of an export are conditional - declaration, specifiers, source
88+
ExportSpecifier: [], // no parts of an export are conditional exported, local
5889
ExpressionStatement: [],
5990
ForStatement: ["update", "body"],
6091
ForInStatement: ["body"],
@@ -64,10 +95,22 @@ const EnrichedVisitorKeys = (function() {
6495
GeneratorExpression: toBeDone(["blocks", "filter", "body"]), // CAUTION: It's deferred to ES7.
6596
Identifier: [],
6697
IfStatement: ["consequent", "alternate"],
67-
ImportDeclaration: toBeDone(["specifiers", "source"]),
68-
ImportDefaultSpecifier: toBeDone(["local"]),
69-
ImportNamespaceSpecifier: toBeDone(["local"]),
70-
ImportSpecifier: toBeDone(["imported", "local"]),
98+
/*
99+
* all parts of an import declaration are executed unconditionally
100+
*/
101+
ImportDeclaration: [], // specifiers, source
102+
/*
103+
* import >>>a<<< from 'module';
104+
*/
105+
ImportDefaultSpecifier: [], // local
106+
/*
107+
* import >>>* as b<<< from 'module';
108+
*/
109+
ImportNamespaceSpecifier: [], // local
110+
/*
111+
* import {>>>a as c<<<,b} from 'module';
112+
*/
113+
ImportSpecifier: [], // imported, local
71114
Literal: [],
72115
LabeledStatement: [],
73116
LogicalExpression: [],
@@ -77,27 +120,45 @@ const EnrichedVisitorKeys = (function() {
77120
ModuleSpecifier: [],
78121
NewExpression: [],
79122
ObjectExpression: [],
80-
ObjectPattern: toBeDone(["properties"]),
123+
/*
124+
* >>>{a,b,c}<<< = {...}
125+
*
126+
* All properties in an object pattern are executed.
127+
*/
128+
ObjectPattern: [], // properties
81129
Program: [],
82130
Property: [],
83-
RestElement: toBeDone(["argument"]),
131+
/*
132+
* argument of the rest element is always executed under the same condition as the rest element itself
133+
*/
134+
RestElement: [], // argument
84135
ReturnStatement: [],
85136
SequenceExpression: [],
86-
SpreadElement: toBeDone(["argument"]),
137+
SpreadElement: [], // the argument of the spread operator always needs to be evaluated - argument
87138
Super: [],
88139
SwitchStatement: [],
89140
SwitchCase: ["test", "consequent"], // test and consequent are executed only conditionally
90-
TaggedTemplateExpression: toBeDone(["tag", "quasi"]),
141+
/*
142+
* all parts of a tagged template literal are executed under the same condition as the context
143+
*/
144+
TaggedTemplateExpression: [], // tag, quasi
91145
TemplateElement: [],
92-
TemplateLiteral: toBeDone(["quasis", "expressions"]),
146+
/*
147+
* all parts of a template literal are executed under the same condition as the context
148+
*/
149+
TemplateLiteral: [], // quasis, expressions
93150
ThisExpression: [],
94151
ThrowStatement: [],
95152
TryStatement: ["handler"], // handler is called conditionally
96153
UnaryExpression: [],
97154
UpdateExpression: [],
98155
VariableDeclaration: [],
99156
VariableDeclarator: [],
100-
WhileStatement: ["body"], // condition is executed on the same conditions as the surrounding block and potentially repeated, block maybe entered only conditionally but can be repeated
157+
/*
158+
* 'condition' is executed on the same conditions as the surrounding block and potentially repeated,
159+
* 'block' maybe entered only conditionally but can be repeated
160+
*/
161+
WhileStatement: ["body"],
101162
WithStatement: [],
102163
YieldExpression: []
103164
};
@@ -151,6 +212,9 @@ const CALL_JQUERY_SAP_IS_DECLARED = [["jQuery", "$"], "sap", "isDeclared"];
151212
const CALL_JQUERY_SAP_REQUIRE = [["jQuery", "$"], "sap", "require"];
152213
const CALL_JQUERY_SAP_REGISTER_PRELOADED_MODULES = [["jQuery", "$"], "sap", "registerPreloadedModules"];
153214

215+
function isCallableExpression(node) {
216+
return node.type == Syntax.FunctionExpression || node.type == Syntax.ArrowFunctionExpression;
217+
}
154218

155219
/**
156220
* Analyzes an already parsed JSDocument to collect information about the contained module(s).
@@ -233,7 +297,7 @@ class JSModuleAnalyzer {
233297
if ( iArg < args.length && args[iArg].type == Syntax.ArrayExpression ) {
234298
iArg++;
235299
}
236-
if ( iArg < args.length && args[iArg].type == Syntax.FunctionExpression ) {
300+
if ( iArg < args.length && isCallableExpression(args[iArg]) ) {
237301
// unconditionally execute the factory function
238302
visit(args[iArg].body, conditional);
239303
}
@@ -256,7 +320,7 @@ class JSModuleAnalyzer {
256320
analyzeDependencyArray(args[iArg].elements, conditional, null);
257321
iArg++;
258322
}
259-
if ( iArg < args.length && args[iArg].type == Syntax.FunctionExpression ) {
323+
if ( iArg < args.length && isCallableExpression(args[iArg]) ) {
260324
// analyze the callback function
261325
visit(args[iArg].body, conditional);
262326
}
@@ -275,7 +339,7 @@ class JSModuleAnalyzer {
275339
let legacyCall = isMethodCall(node, CALL_JQUERY_SAP_REGISTER_PRELOADED_MODULES);
276340
info.setFormat( legacyCall ? ModuleFormat.UI5_LEGACY : ModuleFormat.UI5_DEFINE);
277341
onRegisterPreloadedModules(node, legacyCall);
278-
} else if ( node.callee.type === Syntax.FunctionExpression ) {
342+
} else if ( isCallableExpression(node.callee) ) {
279343
// recognizes a scope function declaration + argument
280344
visit(node.arguments, conditional);
281345
// NODE-TODO defaults of callee?
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
sap.ui.define([
2+
'static/module1'
3+
], (m1) => { // using an arrow function for the module factory
4+
5+
sap.ui.require(['static/module2'], function() {
6+
sap.ui.require(['static/module3'], function() {});
7+
sap.ui.require('no-dependency/module1'); // probing API does not introduce a dependency
8+
});
9+
10+
// using an arrow function for the require callback
11+
sap.ui.require([], () => {
12+
sap.ui.require(['static/module4'], function() {
13+
});
14+
});
15+
16+
// default value in array destructuring
17+
let [exp1 = sap.ui.require(['conditional/module1'], function(){})] = [];
18+
19+
// default value in object destructuring
20+
let {exp2 = sap.ui.require(['conditional/module2'], function(){})} = {};
21+
22+
// dependency embedded in a template
23+
let exp3 = `Some text with an embedded dependency ${sap.ui.require(['static/module5'], function(){})} and further text`;
24+
25+
// dependency embedded in a tagged template
26+
let exp4 = html`Some text with an embedded dependency ${sap.ui.require(['static/module6'], function(){})} and further text`;
27+
28+
// IIAFE (an immediately invoked arrow function expression)
29+
((() => {
30+
sap.ui.require(['static/module7'], function(){});
31+
})());
32+
33+
// a not immediately executed arrow function
34+
let helper = (() => {
35+
sap.ui.require(['conditional/module3'], function(){});
36+
});
37+
38+
});

test/lib/lbt/analyzer/JSModuleAnalyzer.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ function analyzeModule(t, file, name, expectedDependencies, expectDocumentation)
5252
});
5353
}
5454

55+
5556
test.cb("DeclareToplevel", analyzeModule, "modules/declare_toplevel.js", EXPECTED_MODULE_NAME, EXPECTED_DECLARE_DEPENDENCIES);
5657

5758
test.cb("DeclareFunctionExprScope", analyzeModule, "modules/declare_function_expr_scope.js", EXPECTED_MODULE_NAME, EXPECTED_DECLARE_DEPENDENCIES);
@@ -81,3 +82,29 @@ test("Bundle", (t) => {
8182
t.truthy(info.dependencies.every((dep) => !info.isConditionalDependency(dep)), "none of the dependencies must be 'conditional'");
8283
});
8384
});
85+
86+
test("ES6 Syntax", (t) => {
87+
return analyze("modules/es6-syntax.js", "modules/es6-syntax.js").then( (info) => {
88+
const expected = [
89+
"conditional/module1.js",
90+
"conditional/module2.js",
91+
"conditional/module3.js",
92+
"static/module1.js",
93+
"static/module2.js",
94+
"static/module3.js",
95+
"static/module4.js",
96+
"static/module5.js",
97+
"static/module6.js",
98+
"static/module7.js",
99+
"ui5loader-autoconfig.js"
100+
];
101+
const actual = info.dependencies.sort();
102+
t.deepEqual(actual, expected, "module dependencies should match");
103+
expected.forEach((dep) => {
104+
t.is(info.isConditionalDependency(dep), /^conditional\//.test(dep),
105+
"only dependencies to 'conditional/*' modules should be conditional");
106+
t.is(info.isImplicitDependency(dep), !/^(?:conditional|static)\//.test(dep),
107+
"all dependencies other than 'conditional/*' and 'static/*' should be implicit");
108+
});
109+
});
110+
});

0 commit comments

Comments
 (0)