ucn 5.5.2 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +35 -12
- package/.claude/skills/ucn/references/commands.md +21 -17
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +474 -0
- package/README.md +71 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +138 -324
- package/core/account.js +44 -87
- package/core/analysis.js +273 -107
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +29 -65
- package/core/cache.js +101 -16
- package/core/callers.js +352 -70
- package/core/capture.js +331 -0
- package/core/change-kinds.js +888 -0
- package/core/check.js +1117 -139
- package/core/client-receivers.js +189 -0
- package/core/command-contracts.js +7 -6
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +388 -85
- package/core/deletion.js +401 -0
- package/core/discovery.js +104 -9
- package/core/entrypoints.js +12 -6
- package/core/evidence-bands.js +68 -0
- package/core/evidence-bands.json +44 -0
- package/core/execute.js +172 -161
- package/core/hot-summary.js +241 -0
- package/core/index-ir.js +3 -1
- package/core/inprocess-clients.js +41 -15
- package/core/ir.js +5 -1
- package/core/member-signatures.js +789 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +98 -37
- package/core/output/doctor.js +34 -1
- package/core/output/find.js +2 -1
- package/core/output/lines.js +63 -3
- package/core/output/public.js +30 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +21 -2
- package/core/output/search.js +11 -2
- package/core/output/tracing.js +35 -7
- package/core/output-budget.js +124 -57
- package/core/parallel-build.js +3 -3
- package/core/project.js +106 -23
- package/core/provenance-type.js +12 -5
- package/core/public-command.js +35 -9
- package/core/python-modules.js +6 -0
- package/core/reflection.js +166 -23
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/request-helpers.js +63 -7
- package/core/review-items.js +75 -0
- package/core/search.js +89 -128
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3624 -0
- package/core/tracing.js +150 -96
- package/core/verify.js +27 -11
- package/languages/adapter.js +8 -3
- package/languages/c-family.js +12 -0
- package/languages/csharp.js +634 -0
- package/languages/go.js +120 -0
- package/languages/index.js +422 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1460 -2
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1672 -1
- package/languages/rust.js +91 -0
- package/mcp/server.js +44 -110
- package/package.json +11 -6
package/languages/javascript.js
CHANGED
|
@@ -1665,6 +1665,18 @@ function extractInterfaceMembers(interfaceNode, code) {
|
|
|
1665
1665
|
return extractTypeMembers(bodyNode, code);
|
|
1666
1666
|
}
|
|
1667
1667
|
|
|
1668
|
+
/**
|
|
1669
|
+
* A signature in an interface or object type is a member every value of
|
|
1670
|
+
* that type must have, unless it is written optional (`name?`). The `?` is
|
|
1671
|
+
* a token of the signature node, never part of the name or the type.
|
|
1672
|
+
*/
|
|
1673
|
+
function isOptionalSignature(node) {
|
|
1674
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1675
|
+
if (node.child(i).type === '?') return true;
|
|
1676
|
+
}
|
|
1677
|
+
return false;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1668
1680
|
function extractTypeMembers(bodyNode, code) {
|
|
1669
1681
|
const members = [];
|
|
1670
1682
|
for (let i = 0; i < bodyNode.namedChildCount; i++) {
|
|
@@ -1687,6 +1699,7 @@ function extractTypeMembers(bodyNode, code) {
|
|
|
1687
1699
|
sourceSpan,
|
|
1688
1700
|
memberType: 'method',
|
|
1689
1701
|
isMethod: true,
|
|
1702
|
+
...(!isOptionalSignature(child) && { requiredMember: true }),
|
|
1690
1703
|
...typeAnno
|
|
1691
1704
|
});
|
|
1692
1705
|
}
|
|
@@ -1706,6 +1719,7 @@ function extractTypeMembers(bodyNode, code) {
|
|
|
1706
1719
|
endLine,
|
|
1707
1720
|
sourceSpan,
|
|
1708
1721
|
memberType: 'field',
|
|
1722
|
+
...(!isOptionalSignature(child) && { requiredMember: true }),
|
|
1709
1723
|
...(fieldType && { fieldType })
|
|
1710
1724
|
});
|
|
1711
1725
|
}
|
|
@@ -1908,6 +1922,9 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
1908
1922
|
sourceSpan,
|
|
1909
1923
|
memberType: 'abstract',
|
|
1910
1924
|
isMethod: true,
|
|
1925
|
+
// A concrete subclass must supply it (an optional
|
|
1926
|
+
// abstract member is not a thing the language has).
|
|
1927
|
+
requiredMember: true,
|
|
1911
1928
|
...typeAnno,
|
|
1912
1929
|
...(docstring && { docstring }),
|
|
1913
1930
|
...(decorators.length > 0 && { decorators })
|
|
@@ -2234,13 +2251,42 @@ function jsModuleValueAliases(root) {
|
|
|
2234
2251
|
// A `let`/`var` alias is the class only while nothing assigns the
|
|
2235
2252
|
// name again (fix #392): any write to that identifier anywhere in the
|
|
2236
2253
|
// module, or a second declaration, leaves it unbound.
|
|
2254
|
+
// A write inside a function or block that declares the name itself
|
|
2255
|
+
// (`class Local {}; Local = f`, a parameter, a `let`) writes that
|
|
2256
|
+
// declaration, never the module's binding of the same name: only names
|
|
2257
|
+
// the module declares need the scope walk.
|
|
2258
|
+
const moduleNames = new Set();
|
|
2259
|
+
for (let statement of root.namedChildren) {
|
|
2260
|
+
if (statement.type === 'export_statement') statement = statement.childForFieldName('declaration') || statement;
|
|
2261
|
+
if (statement.type === 'import_statement') {
|
|
2262
|
+
for (const name of statement.descendantsOfType('identifier')) moduleNames.add(name.text);
|
|
2263
|
+
} else if (statement.type === 'lexical_declaration' || statement.type === 'variable_declaration') {
|
|
2264
|
+
for (const declarator of statement.namedChildren) {
|
|
2265
|
+
const target = declarator.type === 'variable_declarator' ? declarator.childForFieldName('name') : null;
|
|
2266
|
+
if (!target) continue;
|
|
2267
|
+
if (target.type === 'identifier') moduleNames.add(target.text);
|
|
2268
|
+
else for (const name of target.descendantsOfType(['identifier', 'shorthand_property_identifier_pattern'])) moduleNames.add(name.text);
|
|
2269
|
+
}
|
|
2270
|
+
} else {
|
|
2271
|
+
const name = statement.childForFieldName?.('name');
|
|
2272
|
+
if (name?.type === 'identifier' || name?.type === 'type_identifier') moduleNames.add(name.text);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2237
2275
|
const written = new Set();
|
|
2276
|
+
const scopes = new Map();
|
|
2277
|
+
const write = (node) => {
|
|
2278
|
+
if (moduleNames.has(node.text) && !written.has(node.text)) {
|
|
2279
|
+
const verdict = referenceScope(node, 'javascript', scopes);
|
|
2280
|
+
if (verdict && typeof verdict === 'object' && verdict.local) return;
|
|
2281
|
+
}
|
|
2282
|
+
written.add(node.text);
|
|
2283
|
+
};
|
|
2238
2284
|
const writeTargets = (node) => {
|
|
2239
2285
|
if (!node) return;
|
|
2240
|
-
if (node.type === 'identifier') {
|
|
2286
|
+
if (node.type === 'identifier') { write(node); return; }
|
|
2241
2287
|
if (/pattern$/.test(node.type) || node.type === 'pair_pattern' ||
|
|
2242
2288
|
node.type === 'shorthand_property_identifier_pattern') {
|
|
2243
|
-
if (node.type === 'shorthand_property_identifier_pattern')
|
|
2289
|
+
if (node.type === 'shorthand_property_identifier_pattern') write(node);
|
|
2244
2290
|
for (const child of node.namedChildren) writeTargets(child);
|
|
2245
2291
|
}
|
|
2246
2292
|
};
|
|
@@ -5861,6 +5907,1121 @@ const _JS_LIFECYCLE_METHODS = new Set([
|
|
|
5861
5907
|
'connectedCallback', 'disconnectedCallback', 'attributeChangedCallback', 'adoptedCallback'
|
|
5862
5908
|
]);
|
|
5863
5909
|
|
|
5910
|
+
|
|
5911
|
+
// The functions JavaScript runners declare tests with (mocha, jest, vitest,
|
|
5912
|
+
// node:test and the runners that copy them).
|
|
5913
|
+
const TEST_SUITE_APIS = new Set(['describe', 'suite', 'context', 'xdescribe', 'fdescribe', 'xcontext']);
|
|
5914
|
+
const TEST_CASE_APIS = new Set(['it', 'test', 'specify', 'xit', 'fit', 'xtest', 'xspecify']);
|
|
5915
|
+
// Members that keep a call a declaration of the same thing.
|
|
5916
|
+
const TEST_API_MODIFIERS = new Set(['only', 'skip', 'todo', 'concurrent', 'sequential', 'serial', 'parallel',
|
|
5917
|
+
'failing', 'fails', 'fixme', 'slow']);
|
|
5918
|
+
// Members whose call returns the declaring function: it.each(rows)(title, fn).
|
|
5919
|
+
const TEST_API_TABLES = new Set(['each', 'for']);
|
|
5920
|
+
const TEST_API_CONDITIONS = new Set(['runIf', 'skipIf', 'if']);
|
|
5921
|
+
const FUNCTION_NODES = new Set(['function_declaration', 'function_expression', 'function', 'arrow_function',
|
|
5922
|
+
'generator_function', 'generator_function_declaration', 'method_definition']);
|
|
5923
|
+
// Modules whose import says which runner executes a test file.
|
|
5924
|
+
const TEST_RUNNER_MODULES = new Map([
|
|
5925
|
+
['node:test', 'node-test'], ['test', 'node-test'],
|
|
5926
|
+
['vitest', 'vitest'],
|
|
5927
|
+
['@jest/globals', 'jest'],
|
|
5928
|
+
['mocha', 'mocha'],
|
|
5929
|
+
['bun:test', 'bun'], ['@playwright/test', 'playwright'], ['ava', 'ava'], ['tap', 'tap'],
|
|
5930
|
+
['uvu', 'uvu'], ['@japa/runner', 'japa'], ['cypress', 'cypress'], ['tape', 'tape'],
|
|
5931
|
+
]);
|
|
5932
|
+
// Modules whose default export is itself the function that declares a test.
|
|
5933
|
+
const TEST_CALLABLE_MODULES = new Set(['node:test', 'test', 'ava', 'tape']);
|
|
5934
|
+
// Functions the runners call before the tests of a suite or of a file.
|
|
5935
|
+
const TEST_SETUP_HOOKS = new Set(['before', 'beforeEach', 'beforeAll', 'suiteSetup', 'setup']);
|
|
5936
|
+
// Objects every script can read the runner's globals from.
|
|
5937
|
+
const GLOBAL_OBJECTS = new Set(['globalThis', 'global', 'window', 'self']);
|
|
5938
|
+
const WRAPPERS = new Set(['parenthesized_expression', 'as_expression', 'non_null_expression',
|
|
5939
|
+
'satisfies_expression', 'await_expression', 'type_assertion']);
|
|
5940
|
+
|
|
5941
|
+
function isTestApiName(name) {
|
|
5942
|
+
return TEST_SUITE_APIS.has(name) || TEST_CASE_APIS.has(name);
|
|
5943
|
+
}
|
|
5944
|
+
|
|
5945
|
+
const BLOCK_SCOPES = new Set(['statement_block', 'for_statement', 'for_in_statement', 'catch_clause', 'switch_body']);
|
|
5946
|
+
const CLASS_NODES = new Set(['class_declaration', 'abstract_class_declaration', 'class']);
|
|
5947
|
+
// Declarations that exist for the type checker only.
|
|
5948
|
+
const AMBIENT_NODES = new Set(['ambient_declaration', 'type_alias_declaration', 'interface_declaration',
|
|
5949
|
+
'function_signature']);
|
|
5950
|
+
const DECLARING = new Set(['import_statement', 'lexical_declaration', 'variable_declaration',
|
|
5951
|
+
'function_declaration', 'generator_function_declaration', 'class_declaration', 'export_statement']);
|
|
5952
|
+
// Values that are no function at all.
|
|
5953
|
+
const PLAIN_VALUES = new Set(['number', 'string', 'template_string', 'true', 'false', 'null', 'undefined', 'regex']);
|
|
5954
|
+
const BUILT_VALUES = new Set(['object', 'array', 'class', 'new_expression']);
|
|
5955
|
+
const OTHER = Object.freeze({ other: true });
|
|
5956
|
+
const UNREAD = Object.freeze({ unread: true });
|
|
5957
|
+
// The nodes the binder does something at; any other is only walked through.
|
|
5958
|
+
const BINDER_NODES = new Set([...AMBIENT_NODES, ...FUNCTION_NODES, ...CLASS_NODES, ...BLOCK_SCOPES,
|
|
5959
|
+
'class_static_block', 'with_statement', 'import_statement', 'lexical_declaration', 'variable_declaration',
|
|
5960
|
+
'enum_declaration', 'internal_module', 'module', 'assignment_expression', 'augmented_assignment_expression',
|
|
5961
|
+
'update_expression', 'call_expression']);
|
|
5962
|
+
|
|
5963
|
+
/** Every name a binding pattern introduces. */
|
|
5964
|
+
function patternNames(pattern, out = []) {
|
|
5965
|
+
if (!pattern) return out;
|
|
5966
|
+
switch (pattern.type) {
|
|
5967
|
+
case 'identifier': case 'shorthand_property_identifier_pattern':
|
|
5968
|
+
out.push(pattern.text);
|
|
5969
|
+
return out;
|
|
5970
|
+
case 'assignment_pattern': case 'object_assignment_pattern':
|
|
5971
|
+
return patternNames(pattern.childForFieldName('left'), out);
|
|
5972
|
+
case 'pair_pattern':
|
|
5973
|
+
return patternNames(pattern.childForFieldName('value'), out);
|
|
5974
|
+
case 'required_parameter': case 'optional_parameter':
|
|
5975
|
+
return patternNames(pattern.childForFieldName('pattern'), out);
|
|
5976
|
+
case 'object_pattern': case 'array_pattern': case 'rest_pattern': case 'formal_parameters':
|
|
5977
|
+
for (const child of pattern.namedChildren) patternNames(child, out);
|
|
5978
|
+
return out;
|
|
5979
|
+
default:
|
|
5980
|
+
return out;
|
|
5981
|
+
}
|
|
5982
|
+
}
|
|
5983
|
+
|
|
5984
|
+
/**
|
|
5985
|
+
* What names are bound to, as far as declaring tests goes, by the rules of
|
|
5986
|
+
* the language's own scopes, read once for the whole file. A name belongs
|
|
5987
|
+
* to the scope that declares it: `var` and a function declaration to the
|
|
5988
|
+
* function around them wherever they stand in it, `let`, `const` and a
|
|
5989
|
+
* class to their block, a parameter, a caught value and a loop variable to
|
|
5990
|
+
* what they head. Every write to a name counts, wherever it stands: the
|
|
5991
|
+
* declaration, an assignment (in a nested function too), a destructuring
|
|
5992
|
+
* assignment, an update. A name written once holds that value; a name
|
|
5993
|
+
* written more than once holds a value only when every write gives the
|
|
5994
|
+
* same one, else it is not read. A name no scope of the file binds is the
|
|
5995
|
+
* runner's global when it is spelled like one, unless the file writes that
|
|
5996
|
+
* global or hands the global object to something. Inside `with`, and in a
|
|
5997
|
+
* function that calls `eval`, names can be bound at run time: not read.
|
|
5998
|
+
*
|
|
5999
|
+
* A value is the runner's function under any name ({ api }), a module or
|
|
6000
|
+
* object its functions are read from ({ namespace }), something that is
|
|
6001
|
+
* provably neither ({ other }; `calls` says whether it is a function), or
|
|
6002
|
+
* something this cannot read ({ unread }): a call through the last two is
|
|
6003
|
+
* no test. "helpers.exported(module, name)" says what a project module
|
|
6004
|
+
* exports under a name.
|
|
6005
|
+
*/
|
|
6006
|
+
function createTestBinder(helpers, root) {
|
|
6007
|
+
const literal = node => node && node.type === 'string' ? node.text.slice(1, -1) : null;
|
|
6008
|
+
const scopes = new Map(); // id of the node that opens a scope -> scope
|
|
6009
|
+
// Every call of the file in source order, with the scope it stands in
|
|
6010
|
+
// and the function it stands in; every function, with the one around it.
|
|
6011
|
+
const calls = [];
|
|
6012
|
+
const functions = new Map();
|
|
6013
|
+
const pending = [];
|
|
6014
|
+
const writtenGlobals = new Set();
|
|
6015
|
+
let globalsHandedOver = false;
|
|
6016
|
+
const newScope = parent => ({ parent, names: new Map(), dynamic: false });
|
|
6017
|
+
const bindingIn = (scope, name) => {
|
|
6018
|
+
let binding = scope.names.get(name);
|
|
6019
|
+
if (!binding) {
|
|
6020
|
+
binding = { name, writes: [] };
|
|
6021
|
+
scope.names.set(name, binding);
|
|
6022
|
+
}
|
|
6023
|
+
return binding;
|
|
6024
|
+
};
|
|
6025
|
+
const lookup = (scope, name) => {
|
|
6026
|
+
for (let current = scope; current; current = current.parent) {
|
|
6027
|
+
const binding = current.names.get(name);
|
|
6028
|
+
if (binding) return { binding };
|
|
6029
|
+
if (current.dynamic) return { dynamic: true };
|
|
6030
|
+
}
|
|
6031
|
+
return null;
|
|
6032
|
+
};
|
|
6033
|
+
const top = newScope(null);
|
|
6034
|
+
const fixed = value => ({ kind: 'fixed', value });
|
|
6035
|
+
|
|
6036
|
+
const declarePattern = (target, pattern, value, env) => {
|
|
6037
|
+
if (!pattern) return;
|
|
6038
|
+
if (pattern.type === 'identifier') {
|
|
6039
|
+
const binding = bindingIn(target, pattern.text);
|
|
6040
|
+
if (value) binding.writes.push({ kind: 'value', node: value, env });
|
|
6041
|
+
return;
|
|
6042
|
+
}
|
|
6043
|
+
if (pattern.type !== 'object_pattern') {
|
|
6044
|
+
for (const name of patternNames(pattern)) bindingIn(target, name).writes.push({ kind: 'unknown', node: value, env });
|
|
6045
|
+
return;
|
|
6046
|
+
}
|
|
6047
|
+
for (const property of pattern.namedChildren) {
|
|
6048
|
+
let key = null;
|
|
6049
|
+
let local = null;
|
|
6050
|
+
if (property.type === 'shorthand_property_identifier_pattern') {
|
|
6051
|
+
key = local = property.text;
|
|
6052
|
+
} else if (property.type === 'pair_pattern') {
|
|
6053
|
+
const keyNode = property.childForFieldName('key');
|
|
6054
|
+
const valueNode = property.childForFieldName('value');
|
|
6055
|
+
if (keyNode && keyNode.type !== 'computed_property_name') key = keyNode.text.replace(/^['"]|['"]$/g, '');
|
|
6056
|
+
if (valueNode && valueNode.type === 'identifier') local = valueNode.text;
|
|
6057
|
+
}
|
|
6058
|
+
if (key && local) bindingIn(target, local).writes.push({ kind: 'member', node: value, env, key });
|
|
6059
|
+
else for (const name of patternNames(property)) bindingIn(target, name).writes.push({ kind: 'unknown', node: value, env });
|
|
6060
|
+
}
|
|
6061
|
+
};
|
|
6062
|
+
|
|
6063
|
+
const declareImport = (node, scope) => {
|
|
6064
|
+
// `import type` binds no value.
|
|
6065
|
+
if (node.children.some(child => child.type === 'type')) return;
|
|
6066
|
+
const required = node.namedChildren.find(child => child.type === 'import_require_clause');
|
|
6067
|
+
if (required) {
|
|
6068
|
+
const local = required.namedChildren.find(child => child.type === 'identifier');
|
|
6069
|
+
const from = literal(required.namedChildren.find(child => child.type === 'string'));
|
|
6070
|
+
if (local) bindingIn(scope, local.text).writes.push({ kind: 'import', module: from, imported: null });
|
|
6071
|
+
return;
|
|
6072
|
+
}
|
|
6073
|
+
const moduleName = literal(node.childForFieldName('source'));
|
|
6074
|
+
const clause = node.namedChildren.find(child => child.type === 'import_clause');
|
|
6075
|
+
for (const part of (clause && clause.namedChildren) || []) {
|
|
6076
|
+
if (part.type === 'identifier') {
|
|
6077
|
+
// A default import: the runner module itself where its
|
|
6078
|
+
// default is the declaring function.
|
|
6079
|
+
bindingIn(scope, part.text).writes.push({
|
|
6080
|
+
kind: 'import', module: moduleName,
|
|
6081
|
+
imported: TEST_RUNNER_MODULES.has(moduleName) ? null : 'default',
|
|
6082
|
+
});
|
|
6083
|
+
} else if (part.type === 'namespace_import') {
|
|
6084
|
+
const local = part.namedChildren.find(child => child.type === 'identifier');
|
|
6085
|
+
if (local) bindingIn(scope, local.text).writes.push({ kind: 'import', module: moduleName, imported: null });
|
|
6086
|
+
} else if (part.type === 'named_imports') {
|
|
6087
|
+
for (const spec of part.namedChildren) {
|
|
6088
|
+
const imported = spec.childForFieldName('name');
|
|
6089
|
+
const alias = spec.childForFieldName('alias');
|
|
6090
|
+
if (!imported || spec.children.some(child => child.type === 'type')) continue;
|
|
6091
|
+
bindingIn(scope, (alias || imported).text).writes.push({
|
|
6092
|
+
kind: 'import', module: moduleName, imported: imported.text,
|
|
6093
|
+
});
|
|
6094
|
+
}
|
|
6095
|
+
}
|
|
6096
|
+
}
|
|
6097
|
+
};
|
|
6098
|
+
|
|
6099
|
+
const buildFunction = (node, scope, varScope, around) => {
|
|
6100
|
+
const fn = { parent: around };
|
|
6101
|
+
functions.set(node.id, fn);
|
|
6102
|
+
const declaration = node.type === 'function_declaration' || node.type === 'generator_function_declaration';
|
|
6103
|
+
const name = node.childForFieldName('name');
|
|
6104
|
+
let outer = scope;
|
|
6105
|
+
if (declaration && name) {
|
|
6106
|
+
if (scope === varScope || scope.bodyOf === varScope) {
|
|
6107
|
+
// Hoisted: one binding with the function's `var`s.
|
|
6108
|
+
bindingIn(varScope, name.text).writes.push(fixed({ other: true, calls: true }));
|
|
6109
|
+
} else {
|
|
6110
|
+
// In a block: the block's own, and outside it too where
|
|
6111
|
+
// the file is not strict (not decided here).
|
|
6112
|
+
bindingIn(scope, name.text).writes.push(fixed({ other: true, calls: true }));
|
|
6113
|
+
bindingIn(varScope, name.text).writes.push({ kind: 'maybe', outer: varScope.parent });
|
|
6114
|
+
}
|
|
6115
|
+
} else if (name && node.type !== 'method_definition') {
|
|
6116
|
+
// The name of a function expression is its own, inside it.
|
|
6117
|
+
outer = newScope(scope);
|
|
6118
|
+
bindingIn(outer, name.text).writes.push(fixed({ other: true, calls: true }));
|
|
6119
|
+
}
|
|
6120
|
+
const own = newScope(outer);
|
|
6121
|
+
scopes.set(node.id, own);
|
|
6122
|
+
const parameters = node.childForFieldName('parameters') || node.childForFieldName('parameter');
|
|
6123
|
+
for (const parameter of patternNames(parameters)) bindingIn(own, parameter).writes.push({ kind: 'parameter' });
|
|
6124
|
+
if (parameters) for (const child of parameters.namedChildren) build(child, own, own, fn);
|
|
6125
|
+
const body = node.childForFieldName('body');
|
|
6126
|
+
if (body && body.type === 'statement_block') {
|
|
6127
|
+
const inner = newScope(own);
|
|
6128
|
+
inner.bodyOf = own;
|
|
6129
|
+
scopes.set(body.id, inner);
|
|
6130
|
+
for (const child of body.namedChildren) build(child, inner, own, fn);
|
|
6131
|
+
} else if (body) {
|
|
6132
|
+
build(body, own, own, fn);
|
|
6133
|
+
}
|
|
6134
|
+
};
|
|
6135
|
+
|
|
6136
|
+
const build = (node, scope, varScope, fn) => {
|
|
6137
|
+
// A node with nothing inside declares, writes and calls nothing;
|
|
6138
|
+
// most others only hold the ones that do.
|
|
6139
|
+
if (node.namedChildCount === 0) return;
|
|
6140
|
+
const type = node.type;
|
|
6141
|
+
if (!BINDER_NODES.has(type)) {
|
|
6142
|
+
for (const child of node.namedChildren) build(child, scope, varScope, fn);
|
|
6143
|
+
return;
|
|
6144
|
+
}
|
|
6145
|
+
if (AMBIENT_NODES.has(type)) return;
|
|
6146
|
+
if (FUNCTION_NODES.has(type)) {
|
|
6147
|
+
buildFunction(node, scope, varScope, fn);
|
|
6148
|
+
return;
|
|
6149
|
+
}
|
|
6150
|
+
if (CLASS_NODES.has(type)) {
|
|
6151
|
+
const name = node.childForFieldName('name');
|
|
6152
|
+
let inner = scope;
|
|
6153
|
+
if (name && type === 'class') {
|
|
6154
|
+
inner = newScope(scope);
|
|
6155
|
+
scopes.set(node.id, inner);
|
|
6156
|
+
bindingIn(inner, name.text).writes.push(fixed(OTHER));
|
|
6157
|
+
} else if (name) {
|
|
6158
|
+
bindingIn(scope, name.text).writes.push(fixed(OTHER));
|
|
6159
|
+
}
|
|
6160
|
+
for (const child of node.namedChildren) build(child, inner, varScope, fn);
|
|
6161
|
+
return;
|
|
6162
|
+
}
|
|
6163
|
+
if (type === 'class_static_block') {
|
|
6164
|
+
const own = newScope(scope);
|
|
6165
|
+
scopes.set(node.id, own);
|
|
6166
|
+
for (const child of node.namedChildren) build(child, own, own, fn);
|
|
6167
|
+
return;
|
|
6168
|
+
}
|
|
6169
|
+
if (BLOCK_SCOPES.has(type)) {
|
|
6170
|
+
const own = newScope(scope);
|
|
6171
|
+
scopes.set(node.id, own);
|
|
6172
|
+
if (type === 'catch_clause') {
|
|
6173
|
+
for (const name of patternNames(node.childForFieldName('parameter'))) {
|
|
6174
|
+
bindingIn(own, name).writes.push(fixed(OTHER));
|
|
6175
|
+
}
|
|
6176
|
+
} else if (type === 'for_in_statement') {
|
|
6177
|
+
const left = node.childForFieldName('left');
|
|
6178
|
+
const kind = node.childForFieldName('kind');
|
|
6179
|
+
const names = left && left.type === 'identifier' ? [left.text] : patternNames(left);
|
|
6180
|
+
const from = { kind: 'unknown', node: node.childForFieldName('right'), env: scope };
|
|
6181
|
+
for (const name of names) {
|
|
6182
|
+
if (!kind) pending.push({ name, scope, write: from });
|
|
6183
|
+
else bindingIn(kind.text === 'var' ? varScope : own, name).writes.push(from);
|
|
6184
|
+
}
|
|
6185
|
+
}
|
|
6186
|
+
for (const child of node.namedChildren) build(child, own, varScope, fn);
|
|
6187
|
+
return;
|
|
6188
|
+
}
|
|
6189
|
+
if (type === 'with_statement') {
|
|
6190
|
+
// Any name inside may be a property of the object.
|
|
6191
|
+
const own = newScope(scope);
|
|
6192
|
+
own.dynamic = true;
|
|
6193
|
+
scopes.set(node.id, own);
|
|
6194
|
+
for (const child of node.namedChildren) build(child, own, varScope, fn);
|
|
6195
|
+
return;
|
|
6196
|
+
}
|
|
6197
|
+
if (type === 'import_statement') {
|
|
6198
|
+
declareImport(node, scope);
|
|
6199
|
+
return;
|
|
6200
|
+
}
|
|
6201
|
+
if (type === 'lexical_declaration' || type === 'variable_declaration') {
|
|
6202
|
+
const target = type === 'variable_declaration' ? varScope : scope;
|
|
6203
|
+
for (const declarator of node.namedChildren) {
|
|
6204
|
+
if (declarator.type !== 'variable_declarator') continue;
|
|
6205
|
+
const value = declarator.childForFieldName('value');
|
|
6206
|
+
declarePattern(target, declarator.childForFieldName('name'), value, scope);
|
|
6207
|
+
if (value) build(value, scope, varScope, fn);
|
|
6208
|
+
}
|
|
6209
|
+
return;
|
|
6210
|
+
}
|
|
6211
|
+
if (type === 'enum_declaration' || type === 'internal_module' || type === 'module') {
|
|
6212
|
+
const name = node.childForFieldName('name');
|
|
6213
|
+
if (name && name.type === 'identifier') bindingIn(scope, name.text).writes.push(fixed(OTHER));
|
|
6214
|
+
} else if (type === 'assignment_expression' || type === 'augmented_assignment_expression') {
|
|
6215
|
+
const left = node.childForFieldName('left');
|
|
6216
|
+
const right = node.childForFieldName('right');
|
|
6217
|
+
const plain = type === 'assignment_expression';
|
|
6218
|
+
if (left && left.type === 'identifier') {
|
|
6219
|
+
pending.push({
|
|
6220
|
+
name: left.text, scope,
|
|
6221
|
+
write: plain ? { kind: 'value', node: right, env: scope } : { kind: 'unknown' },
|
|
6222
|
+
});
|
|
6223
|
+
} else if (left && (left.type === 'member_expression' || left.type === 'subscript_expression')) {
|
|
6224
|
+
// globalThis.it = ...: the global of that name is written.
|
|
6225
|
+
const object = left.childForFieldName('object');
|
|
6226
|
+
const property = left.childForFieldName('property') || left.childForFieldName('index');
|
|
6227
|
+
if (object && object.type === 'identifier' && GLOBAL_OBJECTS.has(object.text)) {
|
|
6228
|
+
const name = !property ? null : property.type === 'string' ? literal(property)
|
|
6229
|
+
: left.type === 'member_expression' ? property.text : null;
|
|
6230
|
+
pending.push({ global: name, object: object.text, scope });
|
|
6231
|
+
}
|
|
6232
|
+
} else if (left) {
|
|
6233
|
+
for (const name of patternNames(left)) {
|
|
6234
|
+
pending.push({ name, scope, write: { kind: 'unknown', node: right, env: scope } });
|
|
6235
|
+
}
|
|
6236
|
+
}
|
|
6237
|
+
} else if (type === 'update_expression') {
|
|
6238
|
+
const argument = node.childForFieldName('argument');
|
|
6239
|
+
if (argument && argument.type === 'identifier') {
|
|
6240
|
+
pending.push({ name: argument.text, scope, write: { kind: 'unknown' } });
|
|
6241
|
+
}
|
|
6242
|
+
} else if (type === 'call_expression') {
|
|
6243
|
+
calls.push({ node, scope, fn });
|
|
6244
|
+
const callee = node.childForFieldName('function');
|
|
6245
|
+
if (callee && callee.type === 'identifier' && callee.text === 'eval') pending.push({ evalIn: varScope, scope });
|
|
6246
|
+
// Object.assign(globalThis, ...), Object.defineProperty(global,
|
|
6247
|
+
// 'it', ...): the globals they name are written; which ones,
|
|
6248
|
+
// where the names are not written out, is not read. Any other
|
|
6249
|
+
// call handed the global object is taken to leave the
|
|
6250
|
+
// runner's functions alone.
|
|
6251
|
+
const args = node.childForFieldName('arguments');
|
|
6252
|
+
const values = (args && args.namedChildren) || [];
|
|
6253
|
+
const target = values[0];
|
|
6254
|
+
const method = callee && callee.type === 'member_expression' &&
|
|
6255
|
+
/^(Object|Reflect)$/.test(callee.childForFieldName('object')?.text || '')
|
|
6256
|
+
? callee.childForFieldName('property')?.text : null;
|
|
6257
|
+
if (method && target && target.type === 'identifier' && GLOBAL_OBJECTS.has(target.text)) {
|
|
6258
|
+
const keysOf = object => object.namedChildren.map(member =>
|
|
6259
|
+
member.type === 'shorthand_property_identifier' ? member.text
|
|
6260
|
+
: member.type === 'pair' && member.childForFieldName('key')?.type !== 'computed_property_name'
|
|
6261
|
+
? member.childForFieldName('key').text.replace(/^['"]|['"]$/g, '') : null);
|
|
6262
|
+
let names = null;
|
|
6263
|
+
if (method === 'defineProperty' || method === 'set') {
|
|
6264
|
+
names = [values[1] && values[1].type === 'string' ? literal(values[1]) : null];
|
|
6265
|
+
} else if (method === 'assign' || method === 'defineProperties') {
|
|
6266
|
+
names = values.slice(1).flatMap(source => source.type === 'object' ? keysOf(source) : [null]);
|
|
6267
|
+
}
|
|
6268
|
+
for (const name of names || []) pending.push({ global: name, object: target.text, scope });
|
|
6269
|
+
}
|
|
6270
|
+
}
|
|
6271
|
+
for (const child of node.namedChildren) build(child, scope, varScope, fn);
|
|
6272
|
+
};
|
|
6273
|
+
|
|
6274
|
+
scopes.set(root.id, top);
|
|
6275
|
+
for (const child of root.namedChildren) build(child, top, top, null);
|
|
6276
|
+
for (const entry of pending) {
|
|
6277
|
+
if (entry.write) {
|
|
6278
|
+
const found = lookup(entry.scope, entry.name);
|
|
6279
|
+
if (found && found.binding) found.binding.writes.push(entry.write);
|
|
6280
|
+
else writtenGlobals.add(entry.name);
|
|
6281
|
+
} else if (entry.evalIn) {
|
|
6282
|
+
if (!lookup(entry.scope, 'eval')) entry.evalIn.dynamic = true;
|
|
6283
|
+
} else if (!lookup(entry.scope, entry.object)) {
|
|
6284
|
+
if (entry.global) writtenGlobals.add(entry.global);
|
|
6285
|
+
else globalsHandedOver = true;
|
|
6286
|
+
}
|
|
6287
|
+
}
|
|
6288
|
+
|
|
6289
|
+
const requiredModule = (node, scope) => {
|
|
6290
|
+
if (!node || node.type !== 'call_expression') return null;
|
|
6291
|
+
const callee = node.childForFieldName('function');
|
|
6292
|
+
if (!callee || callee.type !== 'identifier' || callee.text !== 'require' || lookup(scope, 'require')) return null;
|
|
6293
|
+
const args = node.childForFieldName('arguments');
|
|
6294
|
+
return args && args.namedChildren.length === 1 ? literal(args.namedChildren[0]) : null;
|
|
6295
|
+
};
|
|
6296
|
+
/** What a module gives: as a whole (name null) or under one name. */
|
|
6297
|
+
const fromModule = (moduleName, name) => {
|
|
6298
|
+
if (TEST_RUNNER_MODULES.has(moduleName)) {
|
|
6299
|
+
if (name == null) {
|
|
6300
|
+
return {
|
|
6301
|
+
namespace: true, runner: true,
|
|
6302
|
+
...(TEST_CALLABLE_MODULES.has(moduleName) && { callable: 'test' }),
|
|
6303
|
+
};
|
|
6304
|
+
}
|
|
6305
|
+
return isTestApiName(name) ? { api: name, runner: true } : OTHER;
|
|
6306
|
+
}
|
|
6307
|
+
if (name == null) return { namespace: true, module: moduleName };
|
|
6308
|
+
const exported = helpers.exported ? helpers.exported(moduleName, name) : null;
|
|
6309
|
+
if (exported) return exported;
|
|
6310
|
+
// A module that is not read: a name spelled like the runner's
|
|
6311
|
+
// function may be it, and may not.
|
|
6312
|
+
return isTestApiName(name) ? UNREAD : OTHER;
|
|
6313
|
+
};
|
|
6314
|
+
const sameValue = (a, b) => !!a.other === !!b.other && !!a.unread === !!b.unread && a.api === b.api &&
|
|
6315
|
+
!!a.namespace === !!b.namespace && a.module === b.module && !!a.runner === !!b.runner &&
|
|
6316
|
+
a.callable === b.callable && a.calls === b.calls && !!a.global === !!b.global &&
|
|
6317
|
+
(a.modifiers || []).join() === (b.modifiers || []).join();
|
|
6318
|
+
const globalNamed = name => {
|
|
6319
|
+
if (writtenGlobals.has(name) || globalsHandedOver) return UNREAD;
|
|
6320
|
+
return { api: name };
|
|
6321
|
+
};
|
|
6322
|
+
const writeValue = (write, name) => {
|
|
6323
|
+
switch (write.kind) {
|
|
6324
|
+
case 'fixed': return write.value;
|
|
6325
|
+
case 'parameter': return isTestApiName(name) ? UNREAD : OTHER;
|
|
6326
|
+
case 'import': return fromModule(write.module, write.imported);
|
|
6327
|
+
case 'value': return valueOf(write.node, write.env, name);
|
|
6328
|
+
case 'member': {
|
|
6329
|
+
const from = write.node ? resolve(write.node, write.env) : null;
|
|
6330
|
+
if (from && from.namespace && from.module) return fromModule(from.module, write.key);
|
|
6331
|
+
if (from && from.namespace && from.global) return isTestApiName(write.key) ? globalNamed(write.key) : OTHER;
|
|
6332
|
+
if (from && from.namespace) return isTestApiName(write.key) ? { api: write.key, runner: from.runner } : OTHER;
|
|
6333
|
+
if (!write.node || PLAIN_VALUES.has(write.node.type) || (from && (from.api || from.other))) return OTHER;
|
|
6334
|
+
return isTestApiName(write.key) ? UNREAD : OTHER;
|
|
6335
|
+
}
|
|
6336
|
+
case 'maybe': {
|
|
6337
|
+
// A function declared in a block may also be the name
|
|
6338
|
+
// outside it: what that name held before decides.
|
|
6339
|
+
const before = lookup(write.outer, name);
|
|
6340
|
+
const held = before && before.binding ? valueOfBinding(before.binding) : null;
|
|
6341
|
+
if (before && before.dynamic) return UNREAD;
|
|
6342
|
+
if (held) return held.api || held.namespace || held.unread ? UNREAD : OTHER;
|
|
6343
|
+
return isTestApiName(name) ? UNREAD : OTHER;
|
|
6344
|
+
}
|
|
6345
|
+
default:
|
|
6346
|
+
// A value this does not follow (a loop's element, an
|
|
6347
|
+
// update, a pattern): the runner's function only where
|
|
6348
|
+
// the name or what it is taken from says so.
|
|
6349
|
+
return isTestApiName(name) || (write.node && mentionsRunner(write.node, write.env)) ? UNREAD : OTHER;
|
|
6350
|
+
}
|
|
6351
|
+
};
|
|
6352
|
+
const valueOfBinding = binding => {
|
|
6353
|
+
if (binding.value !== undefined) return binding.value;
|
|
6354
|
+
// A write that reads the name itself (x = x.trim()) says nothing
|
|
6355
|
+
// by that: the other writes decide.
|
|
6356
|
+
if (binding.resolving) return OTHER;
|
|
6357
|
+
binding.resolving = true;
|
|
6358
|
+
let value;
|
|
6359
|
+
if (binding.writes.length === 0) {
|
|
6360
|
+
// Declared and never written: undefined.
|
|
6361
|
+
value = { other: true, calls: false };
|
|
6362
|
+
} else {
|
|
6363
|
+
const values = binding.writes.map(write => writeValue(write, binding.name));
|
|
6364
|
+
value = values.every(each => sameValue(each, values[0])) ? values[0]
|
|
6365
|
+
: values.some(each => each.unread || each.api || each.namespace) ? UNREAD : OTHER;
|
|
6366
|
+
}
|
|
6367
|
+
binding.resolving = false;
|
|
6368
|
+
binding.value = value;
|
|
6369
|
+
return value;
|
|
6370
|
+
};
|
|
6371
|
+
/** What an expression stands for, or null when it is none of the forms read. */
|
|
6372
|
+
const resolve = (node, scope, depth = 0) => {
|
|
6373
|
+
if (!node || depth > 8) return null;
|
|
6374
|
+
if (WRAPPERS.has(node.type)) return resolve(node.namedChildren[0], scope, depth + 1);
|
|
6375
|
+
if (node.type === 'identifier' || node.type === 'shorthand_property_identifier') {
|
|
6376
|
+
const found = lookup(scope, node.text);
|
|
6377
|
+
if (found && found.binding) return valueOfBinding(found.binding);
|
|
6378
|
+
if (found && found.dynamic) return UNREAD;
|
|
6379
|
+
if (GLOBAL_OBJECTS.has(node.text)) return { namespace: true, global: true };
|
|
6380
|
+
return isTestApiName(node.text) ? globalNamed(node.text) : null;
|
|
6381
|
+
}
|
|
6382
|
+
if (node.type === 'call_expression') {
|
|
6383
|
+
const name = requiredModule(node, scope);
|
|
6384
|
+
return name == null ? null : fromModule(name, null);
|
|
6385
|
+
}
|
|
6386
|
+
if (node.type === 'member_expression') {
|
|
6387
|
+
const object = resolve(node.childForFieldName('object'), scope, depth + 1);
|
|
6388
|
+
const property = node.childForFieldName('property');
|
|
6389
|
+
if (!object || !property || object.other) return null;
|
|
6390
|
+
if (object.unread) return UNREAD;
|
|
6391
|
+
if (object.api) {
|
|
6392
|
+
if (isTestApiName(property.text)) return { ...object, api: property.text };
|
|
6393
|
+
if (TEST_API_MODIFIERS.has(property.text)) {
|
|
6394
|
+
return { ...object, modifiers: [...(object.modifiers || []), property.text] };
|
|
6395
|
+
}
|
|
6396
|
+
return null;
|
|
6397
|
+
}
|
|
6398
|
+
if (object.namespace) {
|
|
6399
|
+
if (object.module) return fromModule(object.module, property.text);
|
|
6400
|
+
if (isTestApiName(property.text)) {
|
|
6401
|
+
return object.global ? globalNamed(property.text) : { api: property.text, runner: object.runner };
|
|
6402
|
+
}
|
|
6403
|
+
// The callable default of a runner module carries members too.
|
|
6404
|
+
if (object.callable && TEST_API_MODIFIERS.has(property.text)) {
|
|
6405
|
+
return { api: object.callable, runner: object.runner, modifiers: [property.text] };
|
|
6406
|
+
}
|
|
6407
|
+
}
|
|
6408
|
+
}
|
|
6409
|
+
return null;
|
|
6410
|
+
};
|
|
6411
|
+
/** Does an expression name the runner's function or module anywhere? */
|
|
6412
|
+
const mentionsRunner = (node, scope) => {
|
|
6413
|
+
if (FUNCTION_NODES.has(node.type)) return false;
|
|
6414
|
+
if (node.type === 'identifier' || node.type === 'call_expression' || node.type === 'member_expression') {
|
|
6415
|
+
const value = resolve(node, scope);
|
|
6416
|
+
if (value && (value.unread || (value.runner && (value.api || value.namespace)))) return true;
|
|
6417
|
+
}
|
|
6418
|
+
return node.namedChildren.some(child => mentionsRunner(child, scope));
|
|
6419
|
+
};
|
|
6420
|
+
/** A value an initializer stands for, or what an unreadable one leaves. */
|
|
6421
|
+
const valueOf = (node, scope, name) => {
|
|
6422
|
+
const resolved = resolve(node, scope);
|
|
6423
|
+
if (resolved) return resolved;
|
|
6424
|
+
if (!node) return { other: true, calls: false };
|
|
6425
|
+
if (node.type !== 'class' && FUNCTION_NODES.has(node.type)) return { other: true, calls: true };
|
|
6426
|
+
if (PLAIN_VALUES.has(node.type)) return { other: true, calls: false };
|
|
6427
|
+
// A value made here is not the runner's function; one that holds
|
|
6428
|
+
// the runner's names can hand them on.
|
|
6429
|
+
if (BUILT_VALUES.has(node.type)) return mentionsRunner(node, scope) ? UNREAD : { other: true, calls: false };
|
|
6430
|
+
return isTestApiName(name) || mentionsRunner(node, scope) ? UNREAD : OTHER;
|
|
6431
|
+
};
|
|
6432
|
+
/** Is the name a callee starts with one this cannot read? */
|
|
6433
|
+
const unreadAt = (node, scope) => {
|
|
6434
|
+
let base = node;
|
|
6435
|
+
while (base && (base.type === 'member_expression' || base.type === 'call_expression' || WRAPPERS.has(base.type))) {
|
|
6436
|
+
base = base.type === 'member_expression' ? base.childForFieldName('object')
|
|
6437
|
+
: base.type === 'call_expression' ? base.childForFieldName('function') : base.namedChildren[0];
|
|
6438
|
+
}
|
|
6439
|
+
if (!base || base.type !== 'identifier') return false;
|
|
6440
|
+
const value = resolve(base, scope);
|
|
6441
|
+
return !!value && !!value.unread;
|
|
6442
|
+
};
|
|
6443
|
+
/**
|
|
6444
|
+
* The modules the name a callee starts with is taken from (an import,
|
|
6445
|
+
* a require, a member of one), or of a require called in place.
|
|
6446
|
+
*/
|
|
6447
|
+
const modulesOf = (node, scope) => {
|
|
6448
|
+
let base = node;
|
|
6449
|
+
while (base && (base.type === 'member_expression' || WRAPPERS.has(base.type) ||
|
|
6450
|
+
(base.type === 'call_expression' && requiredModule(base, scope) == null))) {
|
|
6451
|
+
base = base.type === 'member_expression' ? base.childForFieldName('object')
|
|
6452
|
+
: base.type === 'call_expression' ? base.childForFieldName('function') : base.namedChildren[0];
|
|
6453
|
+
}
|
|
6454
|
+
if (!base) return [];
|
|
6455
|
+
if (base.type === 'call_expression') return [requiredModule(base, scope)];
|
|
6456
|
+
if (base.type !== 'identifier') return [];
|
|
6457
|
+
const found = lookup(scope, base.text);
|
|
6458
|
+
const out = [];
|
|
6459
|
+
for (const write of (found && found.binding ? found.binding.writes : [])) {
|
|
6460
|
+
if (write.kind === 'import' && write.module != null) out.push(write.module);
|
|
6461
|
+
else if ((write.kind === 'value' || write.kind === 'member') && write.node) {
|
|
6462
|
+
const required = requiredModule(write.node, write.env);
|
|
6463
|
+
if (required != null) out.push(required);
|
|
6464
|
+
}
|
|
6465
|
+
}
|
|
6466
|
+
return out;
|
|
6467
|
+
};
|
|
6468
|
+
/**
|
|
6469
|
+
* Is a callee a runner's setup hook: one of their names, bound by
|
|
6470
|
+
* nothing in the file (the runner's global) or by an import from a
|
|
6471
|
+
* runner's module.
|
|
6472
|
+
*/
|
|
6473
|
+
const setupHookAt = (node, scope) => {
|
|
6474
|
+
if (!node || node.type !== 'identifier' || !TEST_SETUP_HOOKS.has(node.text)) return false;
|
|
6475
|
+
const found = lookup(scope, node.text);
|
|
6476
|
+
if (found && found.dynamic) return true;
|
|
6477
|
+
if (!found || !found.binding) return true;
|
|
6478
|
+
const writes = found.binding.writes;
|
|
6479
|
+
return writes.length > 0 && writes.every(write => {
|
|
6480
|
+
if (write.kind === 'import') return TEST_RUNNER_MODULES.has(write.module);
|
|
6481
|
+
if ((write.kind === 'value' || write.kind === 'member') && write.node) {
|
|
6482
|
+
const required = requiredModule(write.node, write.env);
|
|
6483
|
+
return required != null && TEST_RUNNER_MODULES.has(required);
|
|
6484
|
+
}
|
|
6485
|
+
return false;
|
|
6486
|
+
});
|
|
6487
|
+
};
|
|
6488
|
+
return {
|
|
6489
|
+
top, resolve, fromModule, unreadAt, calls, mentionsRunner, modulesOf, setupHookAt,
|
|
6490
|
+
/** The record of a function node: `parent` is the function around it. */
|
|
6491
|
+
functionOf: node => functions.get(node.id) || null,
|
|
6492
|
+
/** What a name declared at the top of the file holds, or undefined. */
|
|
6493
|
+
bound: name => top.names.has(name) ? valueOfBinding(top.names.get(name)) : undefined,
|
|
6494
|
+
/** The one value a name is bound to where it is used ({ node, scope }), or null. */
|
|
6495
|
+
valueAt: (node, scope) => {
|
|
6496
|
+
if (!node || node.type !== 'identifier') return null;
|
|
6497
|
+
const found = lookup(scope, node.text);
|
|
6498
|
+
const writes = found && !found.dynamic && found.binding ? found.binding.writes : [];
|
|
6499
|
+
return writes.length === 1 && writes[0].kind === 'value' && writes[0].node
|
|
6500
|
+
? { node: writes[0].node, scope: writes[0].env } : null;
|
|
6501
|
+
},
|
|
6502
|
+
};
|
|
6503
|
+
}
|
|
6504
|
+
|
|
6505
|
+
/** The file's binder, built once for every question asked of the file. */
|
|
6506
|
+
function testBinderOf(helpers, tree) {
|
|
6507
|
+
const cache = helpers.cache;
|
|
6508
|
+
if (cache && cache.has('testBinder')) return cache.get('testBinder');
|
|
6509
|
+
const binder = createTestBinder(helpers, tree.rootNode);
|
|
6510
|
+
if (cache) cache.set('testBinder', binder);
|
|
6511
|
+
return binder;
|
|
6512
|
+
}
|
|
6513
|
+
|
|
6514
|
+
/** The declaring call a call node is, or null: { kind, table, rows, skip, todo, only, sometimes }. */
|
|
6515
|
+
function testApiCall(node, resolve) {
|
|
6516
|
+
let callee = node.childForFieldName('function');
|
|
6517
|
+
let table = false;
|
|
6518
|
+
let rows = null;
|
|
6519
|
+
let sometimes = false;
|
|
6520
|
+
if (callee && callee.type === 'call_expression') {
|
|
6521
|
+
// it.each(rows)(...), it.skipIf(cond)(...)
|
|
6522
|
+
const inner = callee.childForFieldName('function');
|
|
6523
|
+
if (!inner || inner.type !== 'member_expression') return null;
|
|
6524
|
+
const property = inner.childForFieldName('property');
|
|
6525
|
+
if (!property) return null;
|
|
6526
|
+
if (TEST_API_TABLES.has(property.text)) {
|
|
6527
|
+
table = true;
|
|
6528
|
+
// The table: the call's argument, or the template it is tagged with.
|
|
6529
|
+
const given = callee.childForFieldName('arguments');
|
|
6530
|
+
rows = !given ? null : given.type === 'template_string' ? given : given.namedChildren[0] || null;
|
|
6531
|
+
} else if (TEST_API_CONDITIONS.has(property.text)) sometimes = true;
|
|
6532
|
+
else return null;
|
|
6533
|
+
callee = inner.childForFieldName('object');
|
|
6534
|
+
}
|
|
6535
|
+
const resolved = resolve(callee);
|
|
6536
|
+
if (!resolved) return null;
|
|
6537
|
+
const api = resolved.api || (resolved.namespace && resolved.callable) || null;
|
|
6538
|
+
if (!api) return null;
|
|
6539
|
+
const modifiers = resolved.modifiers || [];
|
|
6540
|
+
return {
|
|
6541
|
+
kind: TEST_SUITE_APIS.has(api) ? 'suite' : 'test',
|
|
6542
|
+
table, rows, sometimes,
|
|
6543
|
+
skip: /^x/.test(api) || modifiers.includes('skip'),
|
|
6544
|
+
todo: modifiers.includes('todo'),
|
|
6545
|
+
only: modifiers.includes('only') || api === 'fit' || api === 'fdescribe',
|
|
6546
|
+
};
|
|
6547
|
+
}
|
|
6548
|
+
|
|
6549
|
+
/** The value of a title written as a literal, or null. */
|
|
6550
|
+
function literalTitle(node) {
|
|
6551
|
+
if (!node) return null;
|
|
6552
|
+
if (node.type !== 'string' && node.type !== 'template_string') return null;
|
|
6553
|
+
if (node.namedChildren.some(child => child.type === 'template_substitution')) return null;
|
|
6554
|
+
const body = node.text.slice(1, -1);
|
|
6555
|
+
if (!body.includes('\\')) return body;
|
|
6556
|
+
// Escapes that only quote a character; anything else is not read.
|
|
6557
|
+
if (/\\(?!['"\x60\\])/.test(body)) return null;
|
|
6558
|
+
return body.replace(/\\(['"\x60\\])/g, '$1');
|
|
6559
|
+
}
|
|
6560
|
+
|
|
6561
|
+
/** Whether a literal is truthy: true, false, or null when it is no literal. */
|
|
6562
|
+
function literalTruth(node) {
|
|
6563
|
+
if (!node) return null;
|
|
6564
|
+
switch (node.type) {
|
|
6565
|
+
case 'true': return true;
|
|
6566
|
+
case 'false': case 'null': case 'undefined': return false;
|
|
6567
|
+
case 'string': return node.text.length > 2;
|
|
6568
|
+
case 'template_string':
|
|
6569
|
+
return node.namedChildren.some(child => child.type === 'template_substitution') ? null : node.text.length > 2;
|
|
6570
|
+
case 'number': {
|
|
6571
|
+
const value = Number(node.text.replace(/_/g, '').replace(/n$/, ''));
|
|
6572
|
+
return Number.isNaN(value) ? null : value !== 0;
|
|
6573
|
+
}
|
|
6574
|
+
case 'parenthesized_expression': return literalTruth(node.namedChildren[0]);
|
|
6575
|
+
default: return null;
|
|
6576
|
+
}
|
|
6577
|
+
}
|
|
6578
|
+
|
|
6579
|
+
/**
|
|
6580
|
+
* What an options object of a declaring call says: 'skip' (a skip or todo
|
|
6581
|
+
* whose value is a truthy literal), 'sometimes' (one whose value is not a
|
|
6582
|
+
* literal, a shorthand, a spread, a computed key that is not a string) or
|
|
6583
|
+
* null (none, or a falsy literal: an empty reason does not skip).
|
|
6584
|
+
*/
|
|
6585
|
+
function declaredSkip(values, hasCallback) {
|
|
6586
|
+
const options = values.find(value => value.type === 'object');
|
|
6587
|
+
if (!options) return null;
|
|
6588
|
+
let answer = null;
|
|
6589
|
+
const weaker = () => {
|
|
6590
|
+
if (answer !== 'skip') answer = 'sometimes';
|
|
6591
|
+
};
|
|
6592
|
+
for (const pair of options.namedChildren) {
|
|
6593
|
+
if (pair.type === 'spread_element') {
|
|
6594
|
+
weaker();
|
|
6595
|
+
continue;
|
|
6596
|
+
}
|
|
6597
|
+
if (pair.type === 'shorthand_property_identifier') {
|
|
6598
|
+
if (pair.text === 'skip' || (pair.text === 'todo' && !hasCallback)) weaker();
|
|
6599
|
+
continue;
|
|
6600
|
+
}
|
|
6601
|
+
if (pair.type !== 'pair') continue;
|
|
6602
|
+
const key = pair.childForFieldName('key');
|
|
6603
|
+
const value = pair.childForFieldName('value');
|
|
6604
|
+
if (!key || !value) continue;
|
|
6605
|
+
let name;
|
|
6606
|
+
if (key.type === 'computed_property_name') {
|
|
6607
|
+
const inner = key.namedChildren[0];
|
|
6608
|
+
if (!inner || inner.type !== 'string') {
|
|
6609
|
+
weaker();
|
|
6610
|
+
continue;
|
|
6611
|
+
}
|
|
6612
|
+
name = inner.text.slice(1, -1);
|
|
6613
|
+
} else {
|
|
6614
|
+
name = key.text.replace(/^['"]|['"]$/g, '');
|
|
6615
|
+
}
|
|
6616
|
+
if (name !== 'skip' && name !== 'todo') continue;
|
|
6617
|
+
// A todo with a body still runs it.
|
|
6618
|
+
if (name === 'todo' && hasCallback) continue;
|
|
6619
|
+
const truth = literalTruth(value);
|
|
6620
|
+
if (truth === true) answer = 'skip';
|
|
6621
|
+
else if (truth == null) weaker();
|
|
6622
|
+
}
|
|
6623
|
+
return answer;
|
|
6624
|
+
}
|
|
6625
|
+
|
|
6626
|
+
/**
|
|
6627
|
+
* The tests a JavaScript or TypeScript file declares: calls of the
|
|
6628
|
+
* runners' declaring functions, found by what the callee is bound to at
|
|
6629
|
+
* the call (see createTestBinder), named by their literal titles and by
|
|
6630
|
+
* the suites whose callbacks hold them. A title that is not a literal, a
|
|
6631
|
+
* table of cases and a declaration inside a function that is not a
|
|
6632
|
+
* suite's callback are tests whose runtime names are not the written
|
|
6633
|
+
* ones; the last exist only where that function is called.
|
|
6634
|
+
*
|
|
6635
|
+
* The body of a test is whatever is passed for it: a function written in
|
|
6636
|
+
* place, or any other expression (a named function, a member, the result
|
|
6637
|
+
* of a call), which is taken to be a function unless the name is bound in
|
|
6638
|
+
* the file to something that is none. A test given no body at all has
|
|
6639
|
+
* nothing to execute. A test marked skip (or inside a skipped suite) is
|
|
6640
|
+
* declared and not executed; options that are not written out as an
|
|
6641
|
+
* object in the call may skip it. "only" is the mark as written; "parent"
|
|
6642
|
+
* is the index of the suite a unit stands in (-1: the file), so each
|
|
6643
|
+
* runner's own rule for focus can be applied. "open" says the file calls
|
|
6644
|
+
* through a name that may be the runner's function and is not read, or
|
|
6645
|
+
* hands the runner's function to something: it holds tests this did not
|
|
6646
|
+
* see. "calledModules" are the modules whose functions the file calls
|
|
6647
|
+
* outside a test: one that declares tests when called gives them to
|
|
6648
|
+
* this file ("calledWhenCalled": from inside a function of the file, so
|
|
6649
|
+
* to whoever calls that function).
|
|
6650
|
+
*
|
|
6651
|
+
* A declaration exists as often as the statement that makes it runs, up
|
|
6652
|
+
* to the function that holds it: in a branch, a handler or after a
|
|
6653
|
+
* statement that may leave (a return, a throw) it runs only sometimes; in
|
|
6654
|
+
* a loop it stands for several tests, and for none when the loop does not
|
|
6655
|
+
* run. A table of cases is read where it is written out, here or in the
|
|
6656
|
+
* one value its name is bound to: an empty one declares nothing (jest
|
|
6657
|
+
* refuses the file: "emptyTable"), one that is not written out may be
|
|
6658
|
+
* empty.
|
|
6659
|
+
*/
|
|
6660
|
+
function testUnits(fileEntry, helpers) {
|
|
6661
|
+
const tree = helpers.tree();
|
|
6662
|
+
if (!tree) return null;
|
|
6663
|
+
const binder = testBinderOf(helpers, tree);
|
|
6664
|
+
const units = [];
|
|
6665
|
+
// ---- where a declaration stands in the flow of its function ----
|
|
6666
|
+
const CLASS_NODES = new Set(['class_declaration', 'class', 'class_body', 'abstract_class_declaration']);
|
|
6667
|
+
// Can running a statement leave the function it stands in: a return,
|
|
6668
|
+
// a throw that no handler around it catches, process.exit.
|
|
6669
|
+
const leaves = (node, caught = false) => {
|
|
6670
|
+
if (FUNCTION_NODES.has(node.type) || CLASS_NODES.has(node.type)) return false;
|
|
6671
|
+
if (node.type === 'return_statement') return true;
|
|
6672
|
+
if (node.type === 'throw_statement') return !caught;
|
|
6673
|
+
if (node.type === 'call_expression') {
|
|
6674
|
+
const callee = node.childForFieldName('function');
|
|
6675
|
+
if (callee && callee.type === 'member_expression' && callee.childForFieldName('object')?.text === 'process' &&
|
|
6676
|
+
callee.childForFieldName('property')?.text === 'exit') return true;
|
|
6677
|
+
}
|
|
6678
|
+
if (node.type === 'try_statement') {
|
|
6679
|
+
const handled = !!node.childForFieldName('handler');
|
|
6680
|
+
return node.namedChildren.some(child => leaves(child,
|
|
6681
|
+
caught || (handled && sameNode(child, node.childForFieldName('body')))));
|
|
6682
|
+
}
|
|
6683
|
+
return node.namedChildren.some(child => leaves(child, caught));
|
|
6684
|
+
};
|
|
6685
|
+
const leaveMemo = new Map(); // id of a block -> where its first statement that may leave it ends
|
|
6686
|
+
const leavesBefore = (block, statement) => {
|
|
6687
|
+
if (!leaveMemo.has(block.id)) {
|
|
6688
|
+
const first = block.namedChildren.find(statement => leaves(statement));
|
|
6689
|
+
leaveMemo.set(block.id, first ? first.endIndex : Infinity);
|
|
6690
|
+
}
|
|
6691
|
+
return statement.startIndex >= leaveMemo.get(block.id);
|
|
6692
|
+
};
|
|
6693
|
+
const flowMemo = new Map();
|
|
6694
|
+
/**
|
|
6695
|
+
* What the statements around a call say, up to the function or the
|
|
6696
|
+
* file that holds it: it runs only `sometimes`, or as often as a loop
|
|
6697
|
+
* does (`several`).
|
|
6698
|
+
*/
|
|
6699
|
+
const flowOf = (node, scope) => {
|
|
6700
|
+
if (flowMemo.has(node.id)) return flowMemo.get(node.id);
|
|
6701
|
+
let sometimes = false;
|
|
6702
|
+
let several = false;
|
|
6703
|
+
for (let child = node, parent = node.parent; parent; child = parent, parent = parent.parent) {
|
|
6704
|
+
if (FUNCTION_NODES.has(parent.type) || CLASS_NODES.has(parent.type)) break;
|
|
6705
|
+
const is = field => sameNode(parent.childForFieldName(field), child);
|
|
6706
|
+
switch (parent.type) {
|
|
6707
|
+
case 'if_statement': case 'ternary_expression':
|
|
6708
|
+
if (!is('condition')) sometimes = true;
|
|
6709
|
+
break;
|
|
6710
|
+
case 'binary_expression':
|
|
6711
|
+
if (is('right') && /^(&&|\|\||\?\?)$/.test(parent.childForFieldName('operator')?.text || '')) sometimes = true;
|
|
6712
|
+
break;
|
|
6713
|
+
case 'switch_case':
|
|
6714
|
+
if (!is('value')) sometimes = true;
|
|
6715
|
+
break;
|
|
6716
|
+
case 'switch_default': case 'catch_clause':
|
|
6717
|
+
sometimes = true;
|
|
6718
|
+
break;
|
|
6719
|
+
case 'for_statement':
|
|
6720
|
+
if (!is('initializer')) several = sometimes = true;
|
|
6721
|
+
break;
|
|
6722
|
+
case 'for_in_statement':
|
|
6723
|
+
if (is('body')) {
|
|
6724
|
+
several = true;
|
|
6725
|
+
// Over a list written out that holds something, the body runs.
|
|
6726
|
+
if (!parent.children.some(part => part.type === 'of') ||
|
|
6727
|
+
tableRows(parent.childForFieldName('right'), scope) !== 'rows') sometimes = true;
|
|
6728
|
+
}
|
|
6729
|
+
break;
|
|
6730
|
+
case 'while_statement':
|
|
6731
|
+
several = sometimes = true;
|
|
6732
|
+
break;
|
|
6733
|
+
case 'do_statement':
|
|
6734
|
+
// The body runs once at least.
|
|
6735
|
+
several = true;
|
|
6736
|
+
break;
|
|
6737
|
+
case 'statement_block': case 'program':
|
|
6738
|
+
if (leavesBefore(parent, child)) sometimes = true;
|
|
6739
|
+
break;
|
|
6740
|
+
default: break;
|
|
6741
|
+
}
|
|
6742
|
+
}
|
|
6743
|
+
const answer = { sometimes, several };
|
|
6744
|
+
flowMemo.set(node.id, answer);
|
|
6745
|
+
return answer;
|
|
6746
|
+
};
|
|
6747
|
+
/** What a table of cases holds: 'rows', 'empty', or 'unread'. */
|
|
6748
|
+
const tableRows = (node, scope, depth = 0) => {
|
|
6749
|
+
if (!node || depth > 4) return 'unread';
|
|
6750
|
+
switch (node.type) {
|
|
6751
|
+
// A table written as a template has a heading and rows.
|
|
6752
|
+
case 'template_string': return 'rows';
|
|
6753
|
+
case 'parenthesized_expression': case 'as_expression': case 'satisfies_expression': case 'non_null_expression':
|
|
6754
|
+
return tableRows(node.namedChildren[0], scope, depth + 1);
|
|
6755
|
+
case 'array': {
|
|
6756
|
+
const items = node.namedChildren.filter(item => item.type !== 'comment');
|
|
6757
|
+
if (items.length === 0) return 'empty';
|
|
6758
|
+
if (items.some(item => item.type !== 'spread_element')) return 'rows';
|
|
6759
|
+
const parts = items.map(item => tableRows(item.namedChildren[0], scope, depth + 1));
|
|
6760
|
+
return parts.includes('rows') ? 'rows' : parts.every(part => part === 'empty') ? 'empty' : 'unread';
|
|
6761
|
+
}
|
|
6762
|
+
case 'identifier': {
|
|
6763
|
+
const held = binder.valueAt(node, scope);
|
|
6764
|
+
return held ? tableRows(held.node, held.scope, depth + 1) : 'unread';
|
|
6765
|
+
}
|
|
6766
|
+
default: return 'unread';
|
|
6767
|
+
}
|
|
6768
|
+
};
|
|
6769
|
+
let emptyTable = false;
|
|
6770
|
+
// A call through a name that may be the runner's function and is not
|
|
6771
|
+
// read: a test this does not see.
|
|
6772
|
+
let unreadCall = false;
|
|
6773
|
+
let handedOver = false;
|
|
6774
|
+
const calledModules = new Set();
|
|
6775
|
+
const calledWhenCalled = new Set();
|
|
6776
|
+
const stoppedSuites = new Set(); // indexes of suites (-1: the file) a setup hook can skip
|
|
6777
|
+
/** Is a callee written the way a runner's declaring function is called? */
|
|
6778
|
+
const declaringShape = node => {
|
|
6779
|
+
if (!node) return false;
|
|
6780
|
+
if (node.type === 'identifier') return true;
|
|
6781
|
+
if (node.type === 'call_expression') return declaringShape(node.childForFieldName('function'));
|
|
6782
|
+
if (node.type !== 'member_expression') return false;
|
|
6783
|
+
const property = node.childForFieldName('property')?.text || '';
|
|
6784
|
+
return (isTestApiName(property) || TEST_API_MODIFIERS.has(property) || TEST_API_TABLES.has(property) ||
|
|
6785
|
+
TEST_API_CONDITIONS.has(property)) && declaringShape(node.childForFieldName('object'));
|
|
6786
|
+
};
|
|
6787
|
+
/** What an argument after the title is: a body, options, or neither. */
|
|
6788
|
+
const argumentKind = (value, scope) => {
|
|
6789
|
+
if (FUNCTION_NODES.has(value.type)) return 'function';
|
|
6790
|
+
if (value.type === 'object') return 'options';
|
|
6791
|
+
if (PLAIN_VALUES.has(value.type) || value.type === 'array') return 'value';
|
|
6792
|
+
if (value.type === 'spread_element') return 'expression';
|
|
6793
|
+
const held = binder.resolve(value, scope);
|
|
6794
|
+
if (held && held.other && held.calls === false) return 'held';
|
|
6795
|
+
return 'expression';
|
|
6796
|
+
};
|
|
6797
|
+
// Where a call stands: under the suites whose callbacks hold it, inside
|
|
6798
|
+
// a test, or inside a function that is no declaring call's callback
|
|
6799
|
+
// (then what it declares exists only where that function is called).
|
|
6800
|
+
const TOP = { suites: [], inTest: false, indirect: false };
|
|
6801
|
+
const owners = new Map(); // function record -> the declaring call whose callback it is
|
|
6802
|
+
const contexts = new Map();
|
|
6803
|
+
const contextOf = fn => {
|
|
6804
|
+
if (!fn) return TOP;
|
|
6805
|
+
if (!contexts.has(fn)) {
|
|
6806
|
+
const owner = owners.get(fn);
|
|
6807
|
+
contexts.set(fn, owner
|
|
6808
|
+
? {
|
|
6809
|
+
suites: owner.suite ? [...owner.context.suites, owner.suite] : owner.context.suites,
|
|
6810
|
+
inTest: owner.context.inTest || owner.kind === 'test',
|
|
6811
|
+
indirect: owner.context.indirect,
|
|
6812
|
+
}
|
|
6813
|
+
: { ...contextOf(fn.parent), indirect: true });
|
|
6814
|
+
}
|
|
6815
|
+
return contexts.get(fn);
|
|
6816
|
+
};
|
|
6817
|
+
for (const call of binder.calls) {
|
|
6818
|
+
const node = call.node;
|
|
6819
|
+
const api = testApiCall(node, callee => binder.resolve(callee, call.scope));
|
|
6820
|
+
if (!api) {
|
|
6821
|
+
const callee = node.childForFieldName('function');
|
|
6822
|
+
const where = contextOf(call.fn);
|
|
6823
|
+
// Inside a test nothing declares a test of the file: what a
|
|
6824
|
+
// runner takes there is a part of that test.
|
|
6825
|
+
if (where.inTest) continue;
|
|
6826
|
+
// A declaration is given a title or a body, and is called as
|
|
6827
|
+
// the runners' functions are (the name, a member that is one
|
|
6828
|
+
// of their names or marks): any other call through a name
|
|
6829
|
+
// that is not read declares nothing.
|
|
6830
|
+
if ((node.childForFieldName('arguments')?.namedChildCount || 0) > 0 && declaringShape(callee) &&
|
|
6831
|
+
binder.unreadAt(callee, call.scope)) unreadCall = true;
|
|
6832
|
+
// A setup hook that can skip (mocha's this.skip(), a skip of
|
|
6833
|
+
// the context a runner hands it), or whose body is not
|
|
6834
|
+
// written here: the tests of its suite run only sometimes.
|
|
6835
|
+
if (binder.setupHookAt(callee, call.scope)) {
|
|
6836
|
+
const given = (node.childForFieldName('arguments')?.namedChildren || []);
|
|
6837
|
+
const bodies = given.filter(value => FUNCTION_NODES.has(value.type));
|
|
6838
|
+
const unwritten = given.some(value => !FUNCTION_NODES.has(value.type) && !PLAIN_VALUES.has(value.type) &&
|
|
6839
|
+
value.type !== 'object');
|
|
6840
|
+
const skips = part => ((part.type === 'property_identifier' || part.type === 'identifier' ||
|
|
6841
|
+
part.type === 'shorthand_property_identifier_pattern') && part.text === 'skip') || part.namedChildren.some(skips);
|
|
6842
|
+
if (unwritten || bodies.some(skips)) {
|
|
6843
|
+
const around = where.suites;
|
|
6844
|
+
stoppedSuites.add(around.length > 0 ? around[around.length - 1].index : -1);
|
|
6845
|
+
}
|
|
6846
|
+
continue;
|
|
6847
|
+
}
|
|
6848
|
+
// The runner's function handed to something (an argument, a
|
|
6849
|
+
// list it is called through, call and apply): what declares
|
|
6850
|
+
// with it there is not seen.
|
|
6851
|
+
const handed = [node.childForFieldName('arguments'),
|
|
6852
|
+
callee && callee.type === 'member_expression' ? callee.childForFieldName('object') : null];
|
|
6853
|
+
if (handed.some(part => part && part.type !== 'identifier' && binder.mentionsRunner(part, call.scope)) ||
|
|
6854
|
+
(callee && callee.type === 'member_expression' && callee.childForFieldName('object').type === 'identifier' &&
|
|
6855
|
+
/^(call|apply|bind)$/.test(callee.childForFieldName('property')?.text || '') &&
|
|
6856
|
+
binder.mentionsRunner(callee.childForFieldName('object'), call.scope))) handedOver = true;
|
|
6857
|
+
// A function of another module called outside a test: tests
|
|
6858
|
+
// it declares when called are tests of this file, or, from a
|
|
6859
|
+
// function of this file, of whoever calls that.
|
|
6860
|
+
if (callee) {
|
|
6861
|
+
for (const name of binder.modulesOf(callee, call.scope)) (where.indirect ? calledWhenCalled : calledModules).add(name);
|
|
6862
|
+
}
|
|
6863
|
+
continue;
|
|
6864
|
+
}
|
|
6865
|
+
const ctx = contextOf(call.fn);
|
|
6866
|
+
const args = node.childForFieldName('arguments');
|
|
6867
|
+
const values = args ? args.namedChildren : [];
|
|
6868
|
+
const title = literalTitle(values[0]);
|
|
6869
|
+
// The first argument is the title, unless a body, options
|
|
6870
|
+
// or a spread stands there (node: test(fn), test(options, fn)).
|
|
6871
|
+
const rest = values[0] && (FUNCTION_NODES.has(values[0].type) || values[0].type === 'object' ||
|
|
6872
|
+
values[0].type === 'spread_element') ? values : values.slice(1);
|
|
6873
|
+
const kinds = rest.map(value => argumentKind(value, call.scope));
|
|
6874
|
+
const callback = rest.filter((value, i) => kinds[i] === 'function').pop() || null;
|
|
6875
|
+
const given = kinds.filter(kind => kind === 'expression').length;
|
|
6876
|
+
const hasBody = !!callback || given > 0;
|
|
6877
|
+
const dynamic = title == null ? 'name-not-literal'
|
|
6878
|
+
: api.table ? 'generated-cases'
|
|
6879
|
+
: ctx.indirect || ctx.inTest ? 'declared-indirectly' : null;
|
|
6880
|
+
const flow = flowOf(node, call.scope);
|
|
6881
|
+
const rows = api.table ? tableRows(api.rows, call.scope) : 'rows';
|
|
6882
|
+
if (rows === 'empty') emptyTable = true;
|
|
6883
|
+
const option = declaredSkip(rest, hasBody);
|
|
6884
|
+
// Options that are not written out here: a name bound to
|
|
6885
|
+
// something that is no function, or an expression beside
|
|
6886
|
+
// the one that is the body.
|
|
6887
|
+
const optionsUnread = hasBody && (kinds.includes('held') || given > (callback ? 0 : 1));
|
|
6888
|
+
// A test with no body has nothing to execute (pending, todo).
|
|
6889
|
+
const skipped = api.skip || (api.kind === 'test' && !hasBody) || option === 'skip' || rows === 'empty' ||
|
|
6890
|
+
ctx.suites.some(suite => suite.skipped);
|
|
6891
|
+
const sometimes = api.sometimes || option === 'sometimes' || (optionsUnread && !skipped) ||
|
|
6892
|
+
flow.sometimes || rows === 'unread' ||
|
|
6893
|
+
ctx.indirect || ctx.inTest || ctx.suites.some(suite => suite.sometimes);
|
|
6894
|
+
const several = flow.several || ctx.suites.some(suite => suite.several);
|
|
6895
|
+
const index = units.length;
|
|
6896
|
+
const outer = ctx.suites[ctx.suites.length - 1];
|
|
6897
|
+
units.push({
|
|
6898
|
+
kind: api.kind, name: title, titled: true, suite: ctx.suites.map(suite => suite.name),
|
|
6899
|
+
line: node.startPosition.row + 1, startLine: node.startPosition.row + 1,
|
|
6900
|
+
endLine: node.endPosition.row + 1,
|
|
6901
|
+
parent: outer ? outer.index : -1,
|
|
6902
|
+
...(api.only && { only: true }),
|
|
6903
|
+
...(skipped && { skipped: true }),
|
|
6904
|
+
...(sometimes && { conditional: true }),
|
|
6905
|
+
...((dynamic || ctx.suites.some(suite => suite.dynamic)) && {
|
|
6906
|
+
dynamic: dynamic || 'suite-name-not-literal',
|
|
6907
|
+
}),
|
|
6908
|
+
// Declared by a statement that runs more than once.
|
|
6909
|
+
...(several && { several: true }),
|
|
6910
|
+
// Declared where a function of the file is called, by whoever calls it.
|
|
6911
|
+
...(ctx.indirect && { indirect: true }),
|
|
6912
|
+
});
|
|
6913
|
+
// The callback's body is where the suite's own declarations
|
|
6914
|
+
// are written, not a function apart.
|
|
6915
|
+
const held = callback ? binder.functionOf(callback) : null;
|
|
6916
|
+
if (held) {
|
|
6917
|
+
owners.set(held, {
|
|
6918
|
+
context: ctx, kind: api.kind,
|
|
6919
|
+
suite: api.kind === 'suite'
|
|
6920
|
+
? { name: title == null ? '' : title, dynamic: !!dynamic, skipped, sometimes, several, index } : null,
|
|
6921
|
+
});
|
|
6922
|
+
}
|
|
6923
|
+
}
|
|
6924
|
+
// A hook of a suite holds for every test in it, however deep.
|
|
6925
|
+
if (stoppedSuites.size > 0) {
|
|
6926
|
+
for (const unit of units) {
|
|
6927
|
+
if (unit.kind !== 'test' || unit.conditional) continue;
|
|
6928
|
+
for (let at = unit.parent; ; at = units[at].parent) {
|
|
6929
|
+
if (stoppedSuites.has(at)) {
|
|
6930
|
+
unit.conditional = true;
|
|
6931
|
+
break;
|
|
6932
|
+
}
|
|
6933
|
+
if (at === -1) break;
|
|
6934
|
+
}
|
|
6935
|
+
}
|
|
6936
|
+
}
|
|
6937
|
+
return {
|
|
6938
|
+
units,
|
|
6939
|
+
open: unreadCall || handedOver,
|
|
6940
|
+
...(emptyTable && { emptyTable: true }),
|
|
6941
|
+
...(calledModules.size > 0 && { calledModules: [...calledModules].sort() }),
|
|
6942
|
+
...(calledWhenCalled.size > 0 && { calledWhenCalled: [...calledWhenCalled].sort() }),
|
|
6943
|
+
...(units.some(unit => unit.only) && { focus: true }),
|
|
6944
|
+
};
|
|
6945
|
+
}
|
|
6946
|
+
|
|
6947
|
+
/**
|
|
6948
|
+
* What a module exports under a name, as far as declaring tests goes: the
|
|
6949
|
+
* runner's function ({ api }), a namespace of it, something else
|
|
6950
|
+
* ({ other }), or null when no export of that name is read here.
|
|
6951
|
+
*/
|
|
6952
|
+
function testExport(fileEntry, helpers, name) {
|
|
6953
|
+
const tree = helpers.tree();
|
|
6954
|
+
if (!tree) return null;
|
|
6955
|
+
const binder = testBinderOf(helpers, tree);
|
|
6956
|
+
const top = binder.top;
|
|
6957
|
+
const settle = value => value && !value.unread ? value : null;
|
|
6958
|
+
const valueOf = node => {
|
|
6959
|
+
const resolved = binder.resolve(node, top);
|
|
6960
|
+
if (resolved) return resolved;
|
|
6961
|
+
return node && (PLAIN_VALUES.has(node.type) || BUILT_VALUES.has(node.type) || FUNCTION_NODES.has(node.type))
|
|
6962
|
+
? OTHER : null;
|
|
6963
|
+
};
|
|
6964
|
+
const literal = node => node && node.type === 'string' ? node.text.slice(1, -1) : null;
|
|
6965
|
+
let found = null;
|
|
6966
|
+
for (const statement of tree.rootNode.namedChildren) {
|
|
6967
|
+
if (statement.type === 'expression_statement') {
|
|
6968
|
+
// module.exports = ..., module.exports.x = ..., exports.x = ...
|
|
6969
|
+
const assignment = statement.namedChildren[0];
|
|
6970
|
+
if (!assignment || assignment.type !== 'assignment_expression') continue;
|
|
6971
|
+
const left = assignment.childForFieldName('left');
|
|
6972
|
+
const right = assignment.childForFieldName('right');
|
|
6973
|
+
if (!left || !right) continue;
|
|
6974
|
+
const target = left.text.replace(/\s+/g, '');
|
|
6975
|
+
if (target === 'module.exports' || target === 'exports') {
|
|
6976
|
+
if (right.type === 'object') {
|
|
6977
|
+
for (const property of right.namedChildren) {
|
|
6978
|
+
if (property.type === 'shorthand_property_identifier' && property.text === name) {
|
|
6979
|
+
found = valueOf(property);
|
|
6980
|
+
} else if (property.type === 'pair') {
|
|
6981
|
+
const key = property.childForFieldName('key');
|
|
6982
|
+
if (key && key.text.replace(/^['"]|['"]$/g, '') === name) {
|
|
6983
|
+
found = valueOf(property.childForFieldName('value'));
|
|
6984
|
+
}
|
|
6985
|
+
}
|
|
6986
|
+
}
|
|
6987
|
+
} else {
|
|
6988
|
+
const whole = binder.resolve(right, top);
|
|
6989
|
+
if (whole && whole.namespace) {
|
|
6990
|
+
found = whole.module ? binder.fromModule(whole.module, name)
|
|
6991
|
+
: isTestApiName(name) ? { api: name, runner: whole.runner } : OTHER;
|
|
6992
|
+
} else if (whole && whole.api && name === 'default') {
|
|
6993
|
+
found = whole;
|
|
6994
|
+
}
|
|
6995
|
+
}
|
|
6996
|
+
} else if (target === `module.exports.${name}` || target === `exports.${name}`) {
|
|
6997
|
+
found = valueOf(right);
|
|
6998
|
+
}
|
|
6999
|
+
continue;
|
|
7000
|
+
}
|
|
7001
|
+
if (statement.type !== 'export_statement') continue;
|
|
7002
|
+
const source = literal(statement.childForFieldName('source'));
|
|
7003
|
+
const clause = statement.namedChildren.find(child => child.type === 'export_clause');
|
|
7004
|
+
if (clause) {
|
|
7005
|
+
for (const spec of clause.namedChildren) {
|
|
7006
|
+
const local = spec.childForFieldName('name');
|
|
7007
|
+
const alias = spec.childForFieldName('alias');
|
|
7008
|
+
if (!local || (alias || local).text !== name) continue;
|
|
7009
|
+
found = source != null ? binder.fromModule(source, local.text)
|
|
7010
|
+
: (binder.bound(local.text) || valueOf(local));
|
|
7011
|
+
}
|
|
7012
|
+
} else if (source != null && statement.children.some(child => child.type === '*')) {
|
|
7013
|
+
// export * from 'm': the name is whatever that module gives.
|
|
7014
|
+
const given = binder.fromModule(source, name);
|
|
7015
|
+
if (given && !given.other && !given.unread) found = given;
|
|
7016
|
+
} else {
|
|
7017
|
+
const declared = binder.bound(name);
|
|
7018
|
+
const declares = statement.namedChildren.some(child => DECLARING.has(child.type));
|
|
7019
|
+
if (declares && declared) found = declared;
|
|
7020
|
+
}
|
|
7021
|
+
}
|
|
7022
|
+
return settle(found);
|
|
7023
|
+
}
|
|
7024
|
+
|
|
5864
7025
|
/**
|
|
5865
7026
|
* Classify a JS/TS symbol as a runtime entry point of a specific kind.
|
|
5866
7027
|
* Returns 'framework' | null.
|
|
@@ -5889,7 +7050,301 @@ function isEntryPoint(symbol) {
|
|
|
5889
7050
|
return getEntryPointKind(symbol) !== null;
|
|
5890
7051
|
}
|
|
5891
7052
|
|
|
7053
|
+
const CLASS_DECLARATION_NODES = new Set(['class_declaration', 'abstract_class_declaration', 'class']);
|
|
7054
|
+
|
|
7055
|
+
const isStaticMember = member => member.children.some(part => part.type === 'static');
|
|
7056
|
+
const isHiddenMember = node => node.namedChildren.some(part =>
|
|
7057
|
+
part.type === 'accessibility_modifier' && part.text !== 'public');
|
|
7058
|
+
|
|
7059
|
+
/**
|
|
7060
|
+
* Does this class itself declare a private or protected INSTANCE member?
|
|
7061
|
+
* TypeScript then holds its values to that declaration: only its own
|
|
7062
|
+
* instances and those of its descendants are assignable to it. Without
|
|
7063
|
+
* one, anything with the class's members is a value of it: an object
|
|
7064
|
+
* literal, another class, and where a private member is inherited, any
|
|
7065
|
+
* class that inherits the same one. A static member belongs to the
|
|
7066
|
+
* constructor, a constructor parameter with a modifier is a member, and
|
|
7067
|
+
* the constructor's own modifier says who may call it, not what a value
|
|
7068
|
+
* has.
|
|
7069
|
+
*
|
|
7070
|
+
* @param {Set<number>} [ignoredLines] members that start on these lines
|
|
7071
|
+
* are not counted: the class as it was before a change that added them
|
|
7072
|
+
*/
|
|
7073
|
+
function classHasNominalMember(classNode, ignoredLines = null) {
|
|
7074
|
+
const body = classNode && classNode.childForFieldName('body');
|
|
7075
|
+
if (!body) return false;
|
|
7076
|
+
const ignored = node => !!ignoredLines && ignoredLines.has(node.startPosition.row + 1);
|
|
7077
|
+
for (const member of body.namedChildren) {
|
|
7078
|
+
if (ignored(member) || isStaticMember(member)) continue;
|
|
7079
|
+
const name = member.childForFieldName('name');
|
|
7080
|
+
if (name && name.text === 'constructor') {
|
|
7081
|
+
const parameters = member.childForFieldName('parameters');
|
|
7082
|
+
for (const parameter of parameters ? parameters.namedChildren : []) {
|
|
7083
|
+
if (!ignored(parameter) && isHiddenMember(parameter)) return true;
|
|
7084
|
+
}
|
|
7085
|
+
continue;
|
|
7086
|
+
}
|
|
7087
|
+
if (isHiddenMember(member) || (name && name.type === 'private_property_identifier')) return true;
|
|
7088
|
+
}
|
|
7089
|
+
return false;
|
|
7090
|
+
}
|
|
7091
|
+
|
|
7092
|
+
/**
|
|
7093
|
+
* How the member `name` of a class, declared on `line`, takes part in the
|
|
7094
|
+
* class's type: `role` is 'instance' (a member of every value), 'optional'
|
|
7095
|
+
* (`m?(): T`) or 'static' (a member of the constructor); `hidden` is true
|
|
7096
|
+
* for a private, protected or `#` member, which no other code can name or
|
|
7097
|
+
* enumerate. null when the class body has no such member.
|
|
7098
|
+
*/
|
|
7099
|
+
function classMemberRole(classNode, name, line) {
|
|
7100
|
+
const body = classNode && classNode.childForFieldName('body');
|
|
7101
|
+
for (const member of body ? body.namedChildren : []) {
|
|
7102
|
+
const nameNode = member.childForFieldName('name');
|
|
7103
|
+
if (!nameNode || nameNode.text !== name) continue;
|
|
7104
|
+
if (line < member.startPosition.row + 1 || line > member.endPosition.row + 1) continue;
|
|
7105
|
+
const hidden = isHiddenMember(member) || nameNode.type === 'private_property_identifier';
|
|
7106
|
+
if (isStaticMember(member)) return { role: 'static', hidden };
|
|
7107
|
+
return { role: member.children.some(part => part.type === '?') ? 'optional' : 'instance', hidden };
|
|
7108
|
+
}
|
|
7109
|
+
return null;
|
|
7110
|
+
}
|
|
7111
|
+
|
|
7112
|
+
// The comments the TypeScript scanner reads as directives. A directive
|
|
7113
|
+
// starts its comment (`// @ts-ignore`, `/* @ts-ignore */`, the last line
|
|
7114
|
+
// of a block comment) and is matched as a prefix (`@ts-ignored` is one);
|
|
7115
|
+
// `@ts-nocheck` is a pragma of a `//` comment.
|
|
7116
|
+
const TS_DIRECTIVE_LINE = /^\/\/\/?\s*@(?:ts-expect-error|ts-ignore)/;
|
|
7117
|
+
const TS_DIRECTIVE_BLOCK = /^(?:\/|\*)*\s*@(?:ts-expect-error|ts-ignore)/;
|
|
7118
|
+
// A file pragma: `// @name`, the name ending at a space or a colon.
|
|
7119
|
+
const TS_FILE_PRAGMA = /^\/\/\/?\s*@([^\s:]+)/;
|
|
7120
|
+
|
|
7121
|
+
/** Lines that hold a directive comment, and whether the file is unchecked. */
|
|
7122
|
+
function checkDirectives(root) {
|
|
7123
|
+
const lines = new Set();
|
|
7124
|
+
let unchecked = false;
|
|
7125
|
+
for (const comment of root.descendantsOfType('comment')) {
|
|
7126
|
+
const text = comment.text;
|
|
7127
|
+
const directive = text.startsWith('//') ? TS_DIRECTIVE_LINE.test(text)
|
|
7128
|
+
: TS_DIRECTIVE_BLOCK.test(text.slice(text.lastIndexOf('\n') + 1).trimStart());
|
|
7129
|
+
if (directive) lines.add(comment.endPosition.row + 1);
|
|
7130
|
+
}
|
|
7131
|
+
// `@ts-nocheck` and `@ts-check` are pragmas of the `//` comments
|
|
7132
|
+
// before the first token, read without regard to case: the last of
|
|
7133
|
+
// either decides.
|
|
7134
|
+
for (const first of root.children) {
|
|
7135
|
+
if (first.type === 'hash_bang_line') continue;
|
|
7136
|
+
if (first.type !== 'comment') break;
|
|
7137
|
+
const pragma = first.text.startsWith('//') ? TS_FILE_PRAGMA.exec(first.text) : null;
|
|
7138
|
+
const name = pragma ? pragma[1].toLowerCase() : null;
|
|
7139
|
+
if (name === 'ts-nocheck') unchecked = true;
|
|
7140
|
+
else if (name === 'ts-check') unchecked = false;
|
|
7141
|
+
}
|
|
7142
|
+
return { lines, unchecked };
|
|
7143
|
+
}
|
|
7144
|
+
|
|
7145
|
+
/**
|
|
7146
|
+
* The line the compiler reports a break on: the token it underlines.
|
|
7147
|
+
* arity the callee's name (the property of a member call), or the
|
|
7148
|
+
* first argument too many;
|
|
7149
|
+
* import-name the imported name; import-module: the module string;
|
|
7150
|
+
* literal the declared name of the variable the literal is assigned to.
|
|
7151
|
+
* `site.line` is where the engine saw the site; the two differ when a
|
|
7152
|
+
* statement runs over several lines. The call is the one the engine
|
|
7153
|
+
* measured (`site.span`: its source range), whatever name it is called
|
|
7154
|
+
* under and however many calls share its line; an import is the statement
|
|
7155
|
+
* that names `site.module`.
|
|
7156
|
+
*/
|
|
7157
|
+
function reportedLine(root, site) {
|
|
7158
|
+
const line = site.line;
|
|
7159
|
+
const range = [{ row: line - 1, column: 0 }, { row: line, column: 0 }];
|
|
7160
|
+
const spans = node => node.startPosition.row + 1 <= line && node.endPosition.row + 1 >= line;
|
|
7161
|
+
if (site.kind === 'arity') {
|
|
7162
|
+
const calls = site.span ? [nodeAtSpan(root, site.span, CALL_SHAPES)].filter(Boolean)
|
|
7163
|
+
: root.descendantsOfType([...CALL_SHAPES], ...range).filter(call => {
|
|
7164
|
+
const callee = call.childForFieldName(call.type === 'new_expression' ? 'constructor' : 'function');
|
|
7165
|
+
const named = callee && (callee.childForFieldName('property') || callee);
|
|
7166
|
+
return !!named && named.startPosition.row + 1 === line &&
|
|
7167
|
+
(call.type === 'new_expression' || !site.name || named.text === site.name);
|
|
7168
|
+
});
|
|
7169
|
+
for (const call of calls) {
|
|
7170
|
+
const callee = call.childForFieldName(call.type === 'new_expression' ? 'constructor' : 'function');
|
|
7171
|
+
const named = callee && (callee.childForFieldName('property') || callee);
|
|
7172
|
+
if (!named) continue;
|
|
7173
|
+
const given = (call.childForFieldName('arguments')?.namedChildren || [])
|
|
7174
|
+
.filter(argument => argument.type !== 'comment');
|
|
7175
|
+
// `f.apply(self, [..])` is reported at the list; `f.call(self,
|
|
7176
|
+
// ..)` passes its arguments one slot later.
|
|
7177
|
+
if (site.indirect === 'apply' && given[1]) return given[1].startPosition.row + 1;
|
|
7178
|
+
const first = site.indirect === 'call' ? 1 : 0;
|
|
7179
|
+
if (Number.isInteger(site.most) && given.length - first > site.most) {
|
|
7180
|
+
return given[site.most + first].startPosition.row + 1;
|
|
7181
|
+
}
|
|
7182
|
+
return (call.type === 'new_expression' ? call : named).startPosition.row + 1;
|
|
7183
|
+
}
|
|
7184
|
+
return line;
|
|
7185
|
+
}
|
|
7186
|
+
if (site.kind === 'import-name' || site.kind === 'import-module') {
|
|
7187
|
+
const quoted = node => (node ? node.text.slice(1, -1) : null);
|
|
7188
|
+
const statements = root.descendantsOfType(['import_statement', 'export_statement'], ...range).filter(spans);
|
|
7189
|
+
const naming = site.module === undefined ? statements
|
|
7190
|
+
: statements.filter(statement => quoted(statement.childForFieldName('source')) === site.module);
|
|
7191
|
+
for (const statement of naming.length > 0 ? naming : statements) {
|
|
7192
|
+
if (site.kind === 'import-name') {
|
|
7193
|
+
const clause = statement.descendantsOfType('import_clause')[0];
|
|
7194
|
+
const named = site.name === 'default' ? (clause ? clause.namedChildren[0] : null)
|
|
7195
|
+
: statement.descendantsOfType(['identifier', 'property_identifier', 'type_identifier'])
|
|
7196
|
+
.find(node => node.text === site.name);
|
|
7197
|
+
if (named) return named.startPosition.row + 1;
|
|
7198
|
+
}
|
|
7199
|
+
const source = statement.childForFieldName('source');
|
|
7200
|
+
if (source) return source.startPosition.row + 1;
|
|
7201
|
+
}
|
|
7202
|
+
const written = root.descendantsOfType('string', ...range).find(node => node.startPosition.row + 1 >= line);
|
|
7203
|
+
return written ? written.startPosition.row + 1 : line;
|
|
7204
|
+
}
|
|
7205
|
+
if (site.kind === 'literal') {
|
|
7206
|
+
for (const declarator of root.descendantsOfType('variable_declarator', ...range)) {
|
|
7207
|
+
const value = declarator.childForFieldName('value');
|
|
7208
|
+
if (value && value.startPosition.row + 1 === line) {
|
|
7209
|
+
return (declarator.childForFieldName('name') || declarator).startPosition.row + 1;
|
|
7210
|
+
}
|
|
7211
|
+
}
|
|
7212
|
+
}
|
|
7213
|
+
return line;
|
|
7214
|
+
}
|
|
7215
|
+
|
|
7216
|
+
/**
|
|
7217
|
+
* Has the type checker been told not to report this break? As its own
|
|
7218
|
+
* scanner decides: the file carries `@ts-nocheck`, or walking up from the
|
|
7219
|
+
* line the break is reported on, through blank lines and `//` comments
|
|
7220
|
+
* only, there is a line that holds a directive comment.
|
|
7221
|
+
*
|
|
7222
|
+
* @param {object} site - { lines, root, line, kind, name, most, memo }:
|
|
7223
|
+
* `memo` is a Map kept per file for one operation
|
|
7224
|
+
*/
|
|
7225
|
+
function checkSuppressedAt(site) {
|
|
7226
|
+
const { lines, root, memo } = site;
|
|
7227
|
+
if (!root) return false;
|
|
7228
|
+
if (memo && !memo.has('directives')) memo.set('directives', checkDirectives(root));
|
|
7229
|
+
const directives = memo ? memo.get('directives') : checkDirectives(root);
|
|
7230
|
+
if (directives.unchecked) return true;
|
|
7231
|
+
for (let above = reportedLine(root, site) - 1; above >= 1; above--) {
|
|
7232
|
+
if (directives.lines.has(above)) return true;
|
|
7233
|
+
const text = String(lines[above - 1] === undefined ? '' : lines[above - 1]).trim();
|
|
7234
|
+
if (text !== '' && !/^\/\/.*$/.test(text)) return false;
|
|
7235
|
+
}
|
|
7236
|
+
return false;
|
|
7237
|
+
}
|
|
7238
|
+
|
|
7239
|
+
const inTryBlock = node => {
|
|
7240
|
+
for (let up = node.parent; up; up = up.parent) {
|
|
7241
|
+
if (up.type === 'try_statement') return true;
|
|
7242
|
+
if (FUNCTION_SCOPE_NODES.has(up.type)) return false;
|
|
7243
|
+
}
|
|
7244
|
+
return false;
|
|
7245
|
+
};
|
|
7246
|
+
/**
|
|
7247
|
+
* Is the import written on `line` one whose module must be there for the
|
|
7248
|
+
* project to pass its own judge?
|
|
7249
|
+
* typed (TypeScript): an import or export-from statement, `import x =
|
|
7250
|
+
* require(..)`, or `import(..)`: the compiler resolves each of them,
|
|
7251
|
+
* wherever it is written. A `require(..)` call is a call to it.
|
|
7252
|
+
* untyped (JavaScript): the runtime is the judge. The same statements,
|
|
7253
|
+
* and a `require(..)` or `import(..)` call that is not inside a `try`
|
|
7254
|
+
* block, where a missing module is what the block is for.
|
|
7255
|
+
* Several imports can share a line: the one asked about is the one that
|
|
7256
|
+
* names `module`, as it is written.
|
|
7257
|
+
*/
|
|
7258
|
+
function checkedImportAt(root, line, typed, module) {
|
|
7259
|
+
const range = [{ row: line - 1, column: 0 }, { row: line, column: 0 }];
|
|
7260
|
+
const on = node => node.startPosition.row + 1 <= line && node.endPosition.row + 1 >= line;
|
|
7261
|
+
const names = written => module === undefined || (!!written && written.text.slice(1, -1) === module);
|
|
7262
|
+
if (root.descendantsOfType('import_statement', ...range).some(node => on(node) &&
|
|
7263
|
+
names(node.childForFieldName('source') || node.descendantsOfType('string')[0]))) return true;
|
|
7264
|
+
if (root.descendantsOfType('export_statement', ...range)
|
|
7265
|
+
.some(node => on(node) && node.childForFieldName('source') && names(node.childForFieldName('source')))) return true;
|
|
7266
|
+
for (const call of root.descendantsOfType('call_expression', ...range)) {
|
|
7267
|
+
if (!on(call)) continue;
|
|
7268
|
+
const callee = call.childForFieldName('function');
|
|
7269
|
+
if (!callee || !names(call.childForFieldName('arguments')?.namedChildren[0])) continue;
|
|
7270
|
+
if (callee.type === 'import') {
|
|
7271
|
+
if (typed || !inTryBlock(call)) return true;
|
|
7272
|
+
} else if (!typed && callee.type === 'identifier' && callee.text === 'require' && !inTryBlock(call)) {
|
|
7273
|
+
return true;
|
|
7274
|
+
}
|
|
7275
|
+
}
|
|
7276
|
+
return false;
|
|
7277
|
+
}
|
|
7278
|
+
|
|
7279
|
+
const CALL_SHAPES = new Set(['call_expression', 'new_expression']);
|
|
7280
|
+
|
|
7281
|
+
/** The node of one of `types` that covers exactly `span`, or null. */
|
|
7282
|
+
function nodeAtSpan(root, span, types) {
|
|
7283
|
+
for (let node = root.descendantForIndex(span.start, Math.max(span.start, span.end - 1)); node; node = node.parent) {
|
|
7284
|
+
if (node.startIndex === span.start && node.endIndex === span.end && types.has(node.type)) return node;
|
|
7285
|
+
if (node.startIndex < span.start || node.endIndex > span.end) break;
|
|
7286
|
+
}
|
|
7287
|
+
return null;
|
|
7288
|
+
}
|
|
7289
|
+
|
|
7290
|
+
// Members every object has (the `Object` interface): a literal that does
|
|
7291
|
+
// not write `toString` still has one.
|
|
7292
|
+
const OBJECT_MEMBERS = ['constructor', 'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty',
|
|
7293
|
+
'isPrototypeOf', 'propertyIsEnumerable'];
|
|
7294
|
+
|
|
7295
|
+
/**
|
|
7296
|
+
* The name a literal's key gives its member, or null when it cannot be
|
|
7297
|
+
* read off the token: an escape (`"brand"`, `brand`) or a
|
|
7298
|
+
* number (`0x1` is the key "1") spells a name the text is not.
|
|
7299
|
+
*/
|
|
7300
|
+
function literalKeyName(key) {
|
|
7301
|
+
if (key.text.includes('\\')) return null;
|
|
7302
|
+
if (key.type === 'property_identifier' || key.type === 'shorthand_property_identifier') return key.text;
|
|
7303
|
+
if (key.type === 'string') return key.text.slice(1, -1);
|
|
7304
|
+
return null;
|
|
7305
|
+
}
|
|
7306
|
+
|
|
7307
|
+
/**
|
|
7308
|
+
* Members of the object literal that starts on `line` and is the whole
|
|
7309
|
+
* value of a declaration with a type annotation (`const box: Box = {..}`),
|
|
7310
|
+
* with the members every object has. `open`: what the literal has cannot
|
|
7311
|
+
* be read: a spread, a computed, escaped or numeric key; a literal that is
|
|
7312
|
+
* not the declared value itself (an assertion or a call around it decides
|
|
7313
|
+
* its type).
|
|
7314
|
+
* @returns {{names: Set<string>, open: boolean}|null} null: no object
|
|
7315
|
+
* literal starts on that line
|
|
7316
|
+
*/
|
|
7317
|
+
function typedLiteralMembers(root, line) {
|
|
7318
|
+
const objects = root.descendantsOfType('object', { row: line - 1, column: 0 }, { row: line, column: 0 })
|
|
7319
|
+
.filter(node => node.startPosition.row + 1 === line);
|
|
7320
|
+
if (objects.length === 0) return null;
|
|
7321
|
+
const declared = objects.filter(node => {
|
|
7322
|
+
const holder = node.parent;
|
|
7323
|
+
return holder && holder.type === 'variable_declarator' && holder.childForFieldName('type') &&
|
|
7324
|
+
holder.childForFieldName('value')?.id === node.id;
|
|
7325
|
+
});
|
|
7326
|
+
const names = new Set(OBJECT_MEMBERS);
|
|
7327
|
+
if (declared.length !== 1) return { names, open: true };
|
|
7328
|
+
let open = false;
|
|
7329
|
+
for (const member of declared[0].namedChildren) {
|
|
7330
|
+
if (member.type === 'comment') continue;
|
|
7331
|
+
const key = member.type === 'pair' ? member.childForFieldName('key')
|
|
7332
|
+
: member.type === 'method_definition' ? member.childForFieldName('name')
|
|
7333
|
+
: member.type === 'shorthand_property_identifier' ? member : null;
|
|
7334
|
+
const name = key ? literalKeyName(key) : null;
|
|
7335
|
+
if (name !== null) names.add(name);
|
|
7336
|
+
else open = true;
|
|
7337
|
+
}
|
|
7338
|
+
return { names, open };
|
|
7339
|
+
}
|
|
7340
|
+
|
|
5892
7341
|
module.exports = {
|
|
7342
|
+
classHasNominalMember,
|
|
7343
|
+
classMemberRole,
|
|
7344
|
+
typedLiteralMembers,
|
|
7345
|
+
checkSuppressedAt,
|
|
7346
|
+
checkedImportAt,
|
|
7347
|
+
CLASS_DECLARATION_NODES,
|
|
5893
7348
|
findFunctions,
|
|
5894
7349
|
findClasses,
|
|
5895
7350
|
findStateObjects,
|
|
@@ -5901,5 +7356,8 @@ module.exports = {
|
|
|
5901
7356
|
findUsagesInCode,
|
|
5902
7357
|
isEntryPoint,
|
|
5903
7358
|
getEntryPointKind,
|
|
7359
|
+
testUnits,
|
|
7360
|
+
testExport,
|
|
7361
|
+
TEST_RUNNER_MODULES,
|
|
5904
7362
|
parse
|
|
5905
7363
|
};
|