ucn 5.5.3 → 5.6.1

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