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
@@ -342,6 +342,8 @@ function _processFunction(node, functions, processedRanges, lines, code) {
342
342
  ...(paramTypes && { paramTypes }),
343
343
  ...(docstring && { docstring }),
344
344
  ...(decorators.length > 0 && { decorators }),
345
+ ...(decorators.length > 0 && { registrationUnknown: require('../core/python-binding-identity').registrationUnknown(node) }),
346
+ runtimeOwnerPath: require('../core/python-binding-identity').pythonRuntimeOwners(node),
345
347
  ...(isOverloadDecorated(decorators) && { isSignature: true }),
346
348
  ...(nameLine !== decoratorStartLine && { nameLine })
347
349
  });
@@ -389,7 +391,11 @@ function _processClass(node, classes, processedRanges, lines) {
389
391
  members,
390
392
  ...(docstring && { docstring }),
391
393
  ...(decorators.length > 0 && { decorators }),
394
+ ...(decorators.length > 0 && { registrationUnknown: require('../core/python-binding-identity').registrationUnknown(node) }),
395
+ runtimeOwnerPath: require('../core/python-binding-identity').pythonRuntimeOwners(node),
392
396
  ...(bases.length > 0 && { extends: bases.join(', ') }),
397
+ // A protocol class is satisfied by shape: no class has to name it.
398
+ ...(isProtocolClass(node) && { protocolClass: true }),
393
399
  ...(nameLine !== startLine && { nameLine }),
394
400
  ...lexicalScope,
395
401
  });
@@ -987,6 +993,51 @@ function findFunctions(code, parser) {
987
993
  * runtime discards the stub bodies, so pin identity must close over the
988
994
  * group and resolution must prefer the implementation.
989
995
  */
996
+ /**
997
+ * A member a subclass or a structural satisfier must supply: decorated
998
+ * abstract (`@abstractmethod` and its older `abstract*` spellings), or
999
+ * declared in a class that lists `Protocol` among its bases (a protocol
1000
+ * class; a class that merely inherits from one is an implementation).
1001
+ */
1002
+ const PY_ABSTRACT_DECORATORS = new Set(['abstractmethod', 'abstractproperty',
1003
+ 'abstractclassmethod', 'abstractstaticmethod']);
1004
+
1005
+ // The names a module uses for `Protocol` and for the abstract decorators:
1006
+ // the plain spellings, plus what `from m import Protocol as P` binds. A
1007
+ // module alias (`import typing as t`) needs nothing: `t.Protocol` ends in
1008
+ // the plain spelling.
1009
+ const contractSpellingsByTree = new WeakMap();
1010
+ function contractSpellings(node) {
1011
+ let spellings = contractSpellingsByTree.get(node.tree);
1012
+ if (spellings) return spellings;
1013
+ spellings = { protocol: new Set(['Protocol']), abstract: new Set(PY_ABSTRACT_DECORATORS) };
1014
+ for (const statement of node.tree.rootNode.descendantsOfType('import_from_statement')) {
1015
+ for (const part of statement.namedChildren) {
1016
+ if (part.type !== 'aliased_import') continue;
1017
+ const imported = part.childForFieldName('name')?.text.split('.').pop();
1018
+ const alias = part.childForFieldName('alias')?.text;
1019
+ if (!alias) continue;
1020
+ if (imported === 'Protocol') spellings.protocol.add(alias);
1021
+ if (PY_ABSTRACT_DECORATORS.has(imported)) spellings.abstract.add(alias);
1022
+ }
1023
+ }
1024
+ contractSpellingsByTree.set(node.tree, spellings);
1025
+ return spellings;
1026
+ }
1027
+
1028
+ /** Does the class list Protocol among its bases (PEP 544 protocol class)? */
1029
+ function isProtocolClass(classNode) {
1030
+ const { protocol } = contractSpellings(classNode);
1031
+ return extractBases(classNode).some(base => protocol.has(String(base).split('.').pop()));
1032
+ }
1033
+
1034
+ function isRequiredPythonMember(classNode, decorators) {
1035
+ const { abstract } = contractSpellings(classNode);
1036
+ if ((decorators || []).some(decorator =>
1037
+ abstract.has(String(decorator).replace(/\(.*$/s, '').split('.').pop().trim()))) return true;
1038
+ return isProtocolClass(classNode);
1039
+ }
1040
+
990
1041
  function isOverloadDecorated(decorators) {
991
1042
  return decorators.some(d => {
992
1043
  const head = d.split('(')[0].trim();
@@ -1270,7 +1321,10 @@ function extractClassMembers(classNode, code) {
1270
1321
  ...(paramTypes && { paramTypes }),
1271
1322
  ...(docstring && { docstring }),
1272
1323
  ...(memberDecorators.length > 0 && { decorators: memberDecorators }),
1324
+ ...(memberDecorators.length > 0 && { registrationUnknown: require('../core/python-binding-identity').registrationUnknown(funcNode) }),
1325
+ runtimeOwnerPath: require('../core/python-binding-identity').pythonRuntimeOwners(funcNode),
1273
1326
  ...(isOverloadDecorated(memberDecorators) && { isSignature: true }),
1327
+ ...(isRequiredPythonMember(classNode, memberDecorators) && { requiredMember: true }),
1274
1328
  ...(nameLine !== startLine && { nameLine })
1275
1329
  });
1276
1330
  }
@@ -2678,6 +2732,7 @@ function pythonModuleAliases(tree) {
2678
2732
 
2679
2733
  // Annotation positions whose value Python evaluates as an expression.
2680
2734
  const PY_ANNOTATION_OWNERS = new Set(['typed_parameter', 'typed_default_parameter', 'function_definition', 'assignment']);
2735
+ const annotationQuotesByTree = new WeakMap();
2681
2736
 
2682
2737
  /**
2683
2738
  * String annotations holding calls (fix #398F): `def f(v: "Annotated[int,
@@ -2687,9 +2742,13 @@ const PY_ANNOTATION_OWNERS = new Set(['typed_parameter', 'typed_default_paramete
2687
2742
  * interpolation); the result lists the byte ranges of their quotes.
2688
2743
  */
2689
2744
  function annotationStringQuotes(tree) {
2745
+ const cached = annotationQuotesByTree.get(tree);
2746
+ if (cached) return cached;
2690
2747
  const quotes = [];
2691
- for (const holder of getCachedNodeList(tree.rootNode).nodes) {
2692
- if (holder.type !== 'type') continue;
2748
+ // The source tree is immutable. A multi-name query should discover
2749
+ // these file-level ranges once, without rebuilding a flat node list
2750
+ // whenever another file displaces the general traversal cache.
2751
+ for (const holder of tree.rootNode.descendantsOfType('type')) {
2693
2752
  const string = holder.namedChildCount === 1 ? holder.namedChild(0) : null;
2694
2753
  if (string?.type !== 'string' || string.startPosition.row !== string.endPosition.row) continue;
2695
2754
  const owner = holder.parent;
@@ -2706,6 +2765,7 @@ function annotationStringQuotes(tree) {
2706
2765
  quotes.push({ open: parts[0].startIndex, close: parts[2].startIndex,
2707
2766
  contentStart: content.startIndex, contentEnd: content.endIndex });
2708
2767
  }
2768
+ annotationQuotesByTree.set(tree, quotes);
2709
2769
  return quotes;
2710
2770
  }
2711
2771
 
@@ -3834,7 +3894,10 @@ function findCallsInTree(code, tree, parser, sink = null) {
3834
3894
  const callArgsNode = node.childForFieldName('arguments');
3835
3895
  let argCount = 0;
3836
3896
  let argSpread = false;
3837
- if (callArgsNode) {
3897
+ if (callArgsNode?.type === 'generator_expression') {
3898
+ // `f(x for x in xs)`: the bare generator is the one argument.
3899
+ argCount = 1;
3900
+ } else if (callArgsNode) {
3838
3901
  for (let i = 0; i < callArgsNode.namedChildCount; i++) {
3839
3902
  const arg = callArgsNode.namedChild(i);
3840
3903
  if (arg.type.endsWith('comment')) continue;
@@ -5485,6 +5548,1439 @@ function extractConstructorName(node) {
5485
5548
  return null;
5486
5549
  }
5487
5550
 
5551
+
5552
+ // Bases every class may name without bringing a constructor or a test.
5553
+ // (typing.Protocol brings no test and no __new__; the constructor it gives
5554
+ // the class that lists it is read with that class.)
5555
+ const PYTHON_PLAIN_BASES = new Set(['object', 'ABC', 'Generic', 'Protocol']);
5556
+ // The two members by which pytest refuses a class: a constructor that is not object's.
5557
+ const PYTHON_CONSTRUCTORS = ['__init__', '__new__'];
5558
+ // Statements whose blocks run only sometimes.
5559
+ const PY_BRANCHING = new Set(['if_statement', 'elif_clause', 'else_clause', 'try_statement', 'except_clause',
5560
+ 'except_group_clause', 'finally_clause', 'with_statement', 'for_statement', 'while_statement',
5561
+ 'match_statement', 'case_clause']);
5562
+ // What stops a test before its body runs: pytest's and unittest's own
5563
+ // names for skipping, leaving, failing and marking, wherever a setup, a
5564
+ // fixture or a hook mentions one.
5565
+ const PY_STOPPING_NAMES = new Set(['skip', 'skipTest', 'SkipTest', 'importorskip', 'xfail', 'exit', 'fail',
5566
+ 'deselect', 'add_marker', 'applymarker', 'shouldstop', 'shouldfail', 'Skipped', 'Exit', 'getfixturevalue']);
5567
+ const PY_STOPPING_PATHS = /^(pytest\.(skip|importorskip|exit|xfail|fail)|unittest\.(case\.)?SkipTest|_pytest\.outcomes\.\w+|nose\.SkipTest)(\.|$)/;
5568
+ // Names through which a module's or a class's names are written without
5569
+ // an assignment to them.
5570
+ const PY_NAMESPACE_NAMES = new Set(['globals', 'vars', 'locals', 'setattr', 'exec', 'eval', '__dict__', '__setattr__']);
5571
+ const PY_FIXTURE_PATHS = new Set(['pytest.fixture', 'pytest.yield_fixture', 'pytest_asyncio.fixture']);
5572
+ const PY_ABSTRACT_PATHS = new Set(['abc.abstractmethod', 'abc.abstractproperty', 'abc.abstractclassmethod',
5573
+ 'abc.abstractstaticmethod']);
5574
+ // Functions the runner calls by their names before the tests of a module
5575
+ // or of a class.
5576
+ const PY_MODULE_SETUPS = new Set(['setup_module', 'setup_function', 'setUpModule']);
5577
+ const PY_CLASS_SETUPS = new Set(['setup_class', 'setup_method', 'setUp', 'setUpClass', 'asyncSetUp', 'setup']);
5578
+ // pytest hooks called for one test or one collected node: one in a
5579
+ // conftest.py holds for what is collected below that file. Every other
5580
+ // hook is called once for the whole run, whichever conftest.py has it.
5581
+ const PYTEST_NODE_HOOKS = new Set(['pytest_runtest_protocol', 'pytest_runtest_logstart', 'pytest_runtest_logfinish',
5582
+ 'pytest_runtest_setup', 'pytest_runtest_call', 'pytest_runtest_teardown', 'pytest_runtest_makereport',
5583
+ 'pytest_pyfunc_call', 'pytest_collect_file', 'pytest_collect_directory', 'pytest_ignore_collect',
5584
+ 'pytest_pycollect_makemodule', 'pytest_pycollect_makeitem', 'pytest_generate_tests', 'pytest_itemcollected',
5585
+ 'pytest_collectstart', 'pytest_make_collect_report', 'pytest_collectreport', 'pytest_fixture_setup',
5586
+ 'pytest_fixture_post_finalizer', 'pytest_make_parametrize_id']);
5587
+ // pytest hooks whose return value only words a report.
5588
+ const PYTEST_REPORTING_HOOKS = new Set(['pytest_report_header', 'pytest_report_collectionfinish',
5589
+ 'pytest_report_teststatus', 'pytest_make_parametrize_id', 'pytest_assertrepr_compare',
5590
+ 'pytest_report_to_serializable', 'pytest_report_from_serializable']);
5591
+ // What a hook may call on the objects pytest hands it without changing
5592
+ // what is collected or run; the second set gives back another such object.
5593
+ const PYTEST_HOOK_READS = new Set(['getoption', 'getini', 'getvalue', 'addoption', 'addini', 'getgroup',
5594
+ 'addinivalue_line', 'get_closest_marker', 'iter_markers', 'write_line', 'write_sep', 'write', 'section',
5595
+ 'startswith', 'endswith', 'hasplugin', 'has_plugin', 'getplugin', 'get_plugin', 'is_registered', 'isatty']);
5596
+ const PYTEST_HOOK_OBJECTS = new Set(['getgroup', 'get_closest_marker', 'iter_markers', 'getplugin', 'get_plugin']);
5597
+ const PY_TRUTH_TESTS = new Set(['if_statement', 'elif_clause', 'while_statement', 'not_operator', 'boolean_operator',
5598
+ 'comparison_operator', 'conditional_expression', 'assert_statement', 'expression_statement']);
5599
+ // Values that are no function and no class, whatever name holds them.
5600
+ const PY_PLAIN_VALUES = new Set(['none', 'true', 'false', 'integer', 'float', 'string', 'concatenated_string',
5601
+ 'list', 'tuple', 'dictionary', 'set', 'list_comprehension', 'dictionary_comprehension', 'set_comprehension',
5602
+ 'generator_expression', 'ellipsis']);
5603
+
5604
+ /**
5605
+ * What a Python file declares for pytest (measured on pytest 9), read from
5606
+ * this file alone: what the names of the module and of its classes are
5607
+ * bound to once the module has run. Who collects what is settled where
5608
+ * the files are put together (core/test-units.js), because a class runs
5609
+ * the tests of the classes it derives from, a module runs the tests it
5610
+ * imports, and a conftest.py decides for the modules beside it.
5611
+ *
5612
+ * Every write to a name counts, read from the syntax tree in the order
5613
+ * the module runs: the last write that always runs decides, together with
5614
+ * every later one that runs only sometimes (in a branch, a loop, a try).
5615
+ * Of two definitions the last is the one that exists; a definition
5616
+ * followed by `name = None` or `del name` is gone; one inside a branch
5617
+ * exists only sometimes. A name bound to another name of the module (an
5618
+ * alias) or to a lambda is one more test, listed; a test-shaped name bound
5619
+ * to anything else that could be a function or a class, a star import of
5620
+ * a module that is not read, and code that writes the module's names
5621
+ * through globals(), setattr and the like leave tests this does not list
5622
+ * (`inheriting`: the file's count is a lower bound).
5623
+ *
5624
+ * `__test__` is read as pytest reads it: false switches a module, a class
5625
+ * (and every class deriving from it) or a function off; the literal True
5626
+ * makes a class or a function a test whatever it is called; a value that
5627
+ * is not a literal leaves both open (`enabled`, `forced`: 'unknown'). A
5628
+ * write of it that is none of the forms read (through another name, a
5629
+ * string, a function) leaves every switch of the file open.
5630
+ *
5631
+ * A decorator is read by what it is imported as: pytest's and unittest's
5632
+ * marks (skip: declared and not executed; skipif: sometimes; xfail with
5633
+ * run=False: not executed; an empty parametrize: not executed), the
5634
+ * fixtures a test asks for by usefixtures, unittest.mock's patches
5635
+ * (nothing changes), a name of this file bound once to one of pytest's
5636
+ * marks. Any other decorator may do anything: the test is conditional.
5637
+ *
5638
+ * Code that runs before a test's body can stop it: the module's own
5639
+ * statements (pytest.importorskip, pytest.skip, a raise, a function of
5640
+ * the module that does one of these, outside `if __name__ == "__main__"`),
5641
+ * setup functions (setup_module, setUp, ...), fixtures and, in a
5642
+ * conftest.py or a plugin module, hooks. `runtime` says what this file
5643
+ * does as a plugin: whether one of its hooks can change what is collected
5644
+ * or run (it mentions a stopping name, raises, yields, returns a value
5645
+ * where the value decides, or uses what pytest hands it other than by the
5646
+ * few calls that only read), whether such a hook is one pytest calls
5647
+ * once for the whole run (`everywhere`) and not for one test, which
5648
+ * fixtures it defines and whether each can stop a test, and which modules
5649
+ * it loads as plugins. A setup, a fixture or the module itself that calls
5650
+ * a function of another module of the project names the call (`calls`,
5651
+ * `guardCalls`, `skipCalls`), and `stopping` names the module's own
5652
+ * functions that can stop a test: the two are matched where the files are
5653
+ * put together, one module deep (`runtime.hookCalls` for hooks).
5654
+ *
5655
+ * "classes" says, for every class, what it derives from and which
5656
+ * test-shaped members it declares; "functions" every function of the
5657
+ * module's top; "imports" the names it takes from other modules or binds
5658
+ * to other names of its own; "marks" pytest's marks by name, for the
5659
+ * runner's mark expression. A module whose name is outside python_files
5660
+ * is not collected: it declares for others and has no test of its own.
5661
+ */
5662
+ function testUnits(fileEntry, helpers) {
5663
+ const collection = helpers.collection || {};
5664
+ const matches = (patterns, fallback, name) => (patterns && patterns.length > 0 ? patterns : fallback)
5665
+ .some(pattern => /[*?[]/.test(pattern) ? helpers.glob(pattern, name) : name.startsWith(pattern));
5666
+ const functionNamed = name => matches(collection.functions, ['test'], name);
5667
+ const classNamed = name => matches(collection.classes, ['Test'], name);
5668
+ const symbols = fileEntry.symbols || [];
5669
+ const tree = helpers.tree();
5670
+ if (!tree) return null;
5671
+ const root = tree.rootNode;
5672
+
5673
+ // What an imported name stands for: "from unittest import TestCase as
5674
+ // Base" makes Base the TestCase of unittest.
5675
+ const imported = new Map();
5676
+ for (const binding of fileEntry.importBindings || []) {
5677
+ imported.set(binding.alias || binding.name, binding);
5678
+ }
5679
+ const inProject = moduleName => !!(fileEntry.moduleResolved || {})[moduleName];
5680
+ const origin = written => {
5681
+ const parts = String(written).split('.');
5682
+ const binding = imported.get(parts[0]);
5683
+ if (!binding) return { name: parts[parts.length - 1], module: null, bound: false };
5684
+ if (binding.kind !== 'import') {
5685
+ return { name: parts.length > 1 ? parts[parts.length - 1] : binding.name, module: binding.module, bound: true };
5686
+ }
5687
+ return { name: parts[parts.length - 1], module: [binding.module, ...parts.slice(1, -1)].join('.'), bound: true };
5688
+ };
5689
+ const basesOf = cls => (Array.isArray(cls.extends) ? cls.extends : String(cls.extends || '').split(','))
5690
+ .map(base => String(base).trim()).filter(base => base && base !== 'object');
5691
+ const holds = (cls, symbol) => symbol.className === cls.name && symbol.startLine >= cls.startLine &&
5692
+ (symbol.endLine ?? symbol.startLine) <= (cls.endLine ?? cls.startLine);
5693
+
5694
+ // ---- the writes of the module and of each class body, in order ----
5695
+ const dotted = node => {
5696
+ if (!node) return null;
5697
+ if (node.type === 'identifier') return node.text;
5698
+ if (node.type !== 'attribute') return null;
5699
+ const object = dotted(node.childForFieldName('object'));
5700
+ const attribute = node.childForFieldName('attribute');
5701
+ return object && attribute ? `${object}.${attribute.text}` : null;
5702
+ };
5703
+ const scopeWrites = new Map(); // 'module' or a class node's id -> name -> writes
5704
+ const attributeWrites = new Map(); // '<scope>\0<name>' -> writes of <name>.__test__ in that scope
5705
+ const constructorWrites = new Set(); // names of the module whose __init__ or __new__ is written from outside the class
5706
+ const definitions = new Map(); // row of a def's or class's name -> { node, straight, scope }
5707
+ const accounted = new Set(); // start offsets of the __test__ and pytestmark names read as writes
5708
+ const stars = []; // modules whose every name the module takes
5709
+ const note = (scope, name, write) => {
5710
+ if (!scopeWrites.has(scope)) scopeWrites.set(scope, new Map());
5711
+ const names = scopeWrites.get(scope);
5712
+ if (!names.has(name)) names.set(name, []);
5713
+ names.get(name).push(write);
5714
+ };
5715
+ const targetsOf = (node, out = []) => {
5716
+ if (!node) return out;
5717
+ if (node.type === 'identifier') out.push(node);
5718
+ else if (node.type !== 'attribute' && node.type !== 'subscript') {
5719
+ for (const child of node.namedChildren) targetsOf(child, out);
5720
+ }
5721
+ return out;
5722
+ };
5723
+ const visitExpression = (node, scope, straight) => {
5724
+ if (!node) return;
5725
+ if (node.type === 'assignment' || node.type === 'augmented_assignment') {
5726
+ const left = node.childForFieldName('left');
5727
+ let value = node.childForFieldName('right');
5728
+ // a = b = value: every target gets the last value.
5729
+ while (value && value.type === 'assignment') {
5730
+ visitExpression(value, scope, straight);
5731
+ value = value.childForFieldName('right');
5732
+ }
5733
+ const kind = node.type === 'augmented_assignment' ? 'augment' : value ? 'assign' : 'annotation';
5734
+ const write = {
5735
+ kind, value: kind === 'assign' ? value : null, straight, pos: node.startIndex,
5736
+ line: node.startPosition.row + 1, endLine: node.endPosition.row + 1,
5737
+ };
5738
+ if (left && left.type === 'identifier') {
5739
+ if (kind !== 'annotation') {
5740
+ note(scope, left.text, write);
5741
+ accounted.add(left.startIndex);
5742
+ }
5743
+ } else if (left && left.type === 'attribute') {
5744
+ const object = left.childForFieldName('object');
5745
+ const attribute = left.childForFieldName('attribute');
5746
+ if (object && object.type === 'identifier' && attribute && attribute.text === '__test__') {
5747
+ const key = `${scope}\0${object.text}`;
5748
+ if (!attributeWrites.has(key)) attributeWrites.set(key, []);
5749
+ attributeWrites.get(key).push(write);
5750
+ accounted.add(attribute.startIndex);
5751
+ } else if (object && object.type === 'identifier' && attribute && PYTHON_CONSTRUCTORS.includes(attribute.text)) {
5752
+ constructorWrites.add(object.text);
5753
+ }
5754
+ } else {
5755
+ for (const target of targetsOf(left)) note(scope, target.text, { ...write, kind: 'other', value: null });
5756
+ }
5757
+ return;
5758
+ }
5759
+ if (node.type === 'named_expression') {
5760
+ const name = node.childForFieldName('name');
5761
+ if (name) note(scope, name.text, { kind: 'other', value: null, straight, pos: node.startIndex });
5762
+ }
5763
+ };
5764
+ const visitStatement = (statement, scope, straight) => {
5765
+ let node = statement;
5766
+ if (node.type === 'decorated_definition') node = node.childForFieldName('definition') || node;
5767
+ if (node.type === 'function_definition' || node.type === 'class_definition') {
5768
+ const name = node.childForFieldName('name');
5769
+ if (!name) return;
5770
+ const kind = node.type === 'class_definition' ? 'class' : 'def';
5771
+ note(scope, name.text, { kind, value: null, straight, pos: statement.startIndex, node });
5772
+ definitions.set(name.startPosition.row, { node, straight, scope, pos: statement.startIndex, holder: statement });
5773
+ if (kind === 'class') {
5774
+ const body = node.childForFieldName('body');
5775
+ for (const child of body ? body.namedChildren : []) visitStatement(child, node.id, true);
5776
+ }
5777
+ return;
5778
+ }
5779
+ if (node.type === 'expression_statement') {
5780
+ for (const child of node.namedChildren) visitExpression(child, scope, straight);
5781
+ return;
5782
+ }
5783
+ if (node.type === 'import_statement' || node.type === 'import_from_statement') {
5784
+ for (const part of node.namedChildren) {
5785
+ if (part.type === 'wildcard_import') {
5786
+ const from = node.childForFieldName('module_name');
5787
+ if (from && scope === 'module') stars.push({ module: from.text, line: node.startPosition.row + 1, straight });
5788
+ continue;
5789
+ }
5790
+ if (node.type === 'import_from_statement' && sameNode(part, node.childForFieldName('module_name'))) continue;
5791
+ const bound = part.type === 'aliased_import' ? part.childForFieldName('alias')
5792
+ : part.type === 'dotted_name' ? (node.type === 'import_statement' ? part.namedChild(0)
5793
+ : part.namedChild(part.namedChildCount - 1)) : null;
5794
+ if (bound) note(scope, bound.text, { kind: 'import', value: null, straight, pos: node.startIndex, line: node.startPosition.row + 1 });
5795
+ }
5796
+ return;
5797
+ }
5798
+ if (node.type === 'delete_statement') {
5799
+ for (const target of targetsOf(node)) {
5800
+ note(scope, target.text, { kind: 'del', value: null, straight, pos: node.startIndex });
5801
+ accounted.add(target.startIndex);
5802
+ }
5803
+ return;
5804
+ }
5805
+ if (!PY_BRANCHING.has(node.type) && node.type !== 'block') return;
5806
+ if (node.type === 'for_statement') {
5807
+ for (const target of targetsOf(node.childForFieldName('left'))) {
5808
+ note(scope, target.text, { kind: 'other', value: null, straight: false, pos: node.startIndex });
5809
+ }
5810
+ }
5811
+ for (const child of node.namedChildren) {
5812
+ if (child.type === 'block') {
5813
+ for (const inner of child.namedChildren) visitStatement(inner, scope, false);
5814
+ } else if (PY_BRANCHING.has(child.type)) {
5815
+ visitStatement(child, scope, false);
5816
+ } else if (child.type === 'as_pattern' || child.type === 'with_clause') {
5817
+ // with ... as name, except ... as name
5818
+ const walk = part => {
5819
+ if (part.type === 'as_pattern_target') {
5820
+ for (const target of targetsOf(part)) {
5821
+ note(scope, target.text, { kind: 'other', value: null, straight: false, pos: part.startIndex });
5822
+ }
5823
+ return;
5824
+ }
5825
+ for (const next of part.namedChildren) walk(next);
5826
+ };
5827
+ walk(child);
5828
+ }
5829
+ }
5830
+ };
5831
+ for (const statement of root.namedChildren) visitStatement(statement, 'module', true);
5832
+ const writesOf = (scope, name) => (scopeWrites.get(scope) || new Map()).get(name) || [];
5833
+ /** The writes that may be the last: the last one that always runs, and every later one. */
5834
+ const candidatesOf = writes => {
5835
+ if (!writes || writes.length === 0) return [];
5836
+ let last = -1;
5837
+ writes.forEach((write, i) => {
5838
+ if (write.straight) last = i;
5839
+ });
5840
+ return last === -1 ? [null, ...writes] : writes.slice(last);
5841
+ };
5842
+ /** The one write a name has for certain, or null. */
5843
+ const settled = (scope, name) => {
5844
+ const candidates = candidatesOf(writesOf(scope, name));
5845
+ return candidates.length === 1 && candidates[0] ? candidates[0] : null;
5846
+ };
5847
+ /**
5848
+ * The full path a written name stands for: by the file's imports, and
5849
+ * through the names of the module bound once to such a name.
5850
+ */
5851
+ const pathOf = (written, depth = 0) => {
5852
+ const parts = String(written).split('.');
5853
+ const binding = imported.get(parts[0]);
5854
+ if (binding) {
5855
+ if (binding.kind === 'import') return [binding.module, ...parts.slice(1)].join('.');
5856
+ return [binding.module, binding.name, ...parts.slice(1)].join('.');
5857
+ }
5858
+ const write = depth < 4 ? settled('module', parts[0]) : null;
5859
+ const target = write && write.kind === 'assign' ? dotted(write.value) : null;
5860
+ return target ? pathOf([target, ...parts.slice(1)].join('.'), depth + 1) : null;
5861
+ };
5862
+ const unwrapped = node => {
5863
+ let current = node;
5864
+ while (current && current.type === 'parenthesized_expression') current = current.namedChildren[0];
5865
+ return current;
5866
+ };
5867
+ const truthOf = node => {
5868
+ const value = unwrapped(node);
5869
+ if (!value) return null;
5870
+ switch (value.type) {
5871
+ case 'true': return true;
5872
+ case 'false': case 'none': return false;
5873
+ case 'integer': case 'float': return Number(value.text.replace(/_/g, '')) !== 0;
5874
+ case 'string': return value.namedChildren.some(child => child.type === 'interpolation') ? null
5875
+ : value.namedChildren.some(child => child.type === 'string_content');
5876
+ default: return null;
5877
+ }
5878
+ };
5879
+ const stringOf = node => {
5880
+ const value = unwrapped(node);
5881
+ if (!value || value.type !== 'string' || value.namedChildren.some(child => child.type === 'interpolation')) return null;
5882
+ return value.namedChildren.filter(child => child.type === 'string_content').map(child => child.text).join('');
5883
+ };
5884
+ /** A written-out list or tuple, or what a name of the module bound once to one holds. */
5885
+ const sequenceOf = (node, depth = 0) => {
5886
+ const value = unwrapped(node);
5887
+ if (!value) return null;
5888
+ if (value.type === 'list' || value.type === 'tuple') return value;
5889
+ if (value.type === 'identifier' && depth < 4 && !imported.has(value.text)) {
5890
+ const write = settled('module', value.text);
5891
+ return write && write.kind === 'assign' ? sequenceOf(write.value, depth + 1) : null;
5892
+ }
5893
+ return null;
5894
+ };
5895
+ const UNSET = Symbol('unset');
5896
+ const OPEN_SWITCH = Object.freeze({ on: 'unknown', forced: 'unknown' });
5897
+ /**
5898
+ * What `__test__` is once its scope has run: `on` (true, false,
5899
+ * 'unknown') and `forced` (it is the literal True: pytest takes the
5900
+ * class or function whatever it is called). Undefined: never written.
5901
+ */
5902
+ const switchValue = writes => {
5903
+ const candidates = candidatesOf(writes);
5904
+ if (candidates.length === 0) return undefined;
5905
+ const values = candidates.map(write => {
5906
+ if (write === null || write.kind === 'del') return UNSET;
5907
+ const truth = write.kind === 'assign' ? truthOf(write.value) : null;
5908
+ if (truth == null) return OPEN_SWITCH;
5909
+ return { on: truth, forced: truth && unwrapped(write.value).type === 'true' };
5910
+ });
5911
+ if (values.every(value => value === UNSET)) return undefined;
5912
+ if (values.some(value => value === UNSET)) return OPEN_SWITCH;
5913
+ const same = key => values.every(value => value[key] === values[0][key]) ? values[0][key] : 'unknown';
5914
+ return { on: same('on'), forced: same('forced') };
5915
+ };
5916
+
5917
+ // Any use of the two names that is none of the writes read, a string
5918
+ // that spells one: nothing about them is settled.
5919
+ let switchUnsettled = false;
5920
+ let marksUnsettled = false;
5921
+ const scan = node => {
5922
+ if (node.type === 'identifier' && !accounted.has(node.startIndex)) {
5923
+ if (node.text === '__test__') switchUnsettled = true;
5924
+ else if (node.text === 'pytestmark') marksUnsettled = true;
5925
+ } else if (node.type === 'string_content') {
5926
+ if (node.text === '__test__') switchUnsettled = true;
5927
+ else if (node.text === 'pytestmark') marksUnsettled = true;
5928
+ }
5929
+ for (const child of node.namedChildren) scan(child);
5930
+ };
5931
+ if (/__test__|pytestmark/.test(root.text)) scan(root);
5932
+ // A switch written on a name is read only where the name is one
5933
+ // definition or one import of its scope: any other object is not followed.
5934
+ // A name the module never writes and takes by a star import is one.
5935
+ const starBound = (scope, name) => scope === 'module' && stars.length > 0 && !imported.has(name) &&
5936
+ writesOf('module', name).length === 0;
5937
+ for (const key of attributeWrites.keys()) {
5938
+ const [scope, name] = key.split('\0');
5939
+ const scopeKey = scope === 'module' ? 'module' : Number(scope);
5940
+ const writes = writesOf(scopeKey, name);
5941
+ if (starBound(scopeKey, name)) continue;
5942
+ if (writes.length !== 1 || !['def', 'class', 'import'].includes(writes[0].kind)) switchUnsettled = true;
5943
+ }
5944
+ const attributeSwitch = (scope, name) => switchValue(attributeWrites.get(`${scope}\0${name}`));
5945
+
5946
+ // ---- rows of parameters ----
5947
+ const functionsNamed = new Map(); // names of the module's top -> their definition nodes
5948
+ for (const info of definitions.values()) {
5949
+ if (info.scope !== 'module' || info.node.type !== 'function_definition') continue;
5950
+ const name = info.node.childForFieldName('name').text;
5951
+ if (!functionsNamed.has(name)) functionsNamed.set(name, []);
5952
+ functionsNamed.get(name).push(info.node);
5953
+ }
5954
+ const parameterNames = fn => {
5955
+ const names = [];
5956
+ const list = fn.childForFieldName('parameters');
5957
+ for (const parameter of list ? list.namedChildren : []) {
5958
+ let name = parameter.type === 'identifier' ? parameter : parameter.childForFieldName('name');
5959
+ if (!name && (parameter.type === 'list_splat_pattern' || parameter.type === 'dictionary_splat_pattern')) {
5960
+ name = parameter.namedChildren[0];
5961
+ }
5962
+ if (name && name.type === 'identifier') names.push(name.text);
5963
+ }
5964
+ return names;
5965
+ };
5966
+ // Names some class body writes: in a decorator inside that class the
5967
+ // name is the class's, which is not followed.
5968
+ const classWritten = new Set();
5969
+ for (const [scope, names] of scopeWrites) {
5970
+ if (scope !== 'module') for (const name of names.keys()) classWritten.add(name);
5971
+ }
5972
+ const RUN = 'run';
5973
+ /**
5974
+ * What a row of a table of parameters does to the body of the test:
5975
+ * 'run' (the body executes with it), 'skip' (the row's own marks leave
5976
+ * the body out), 'sometimes', or { via } where a name of another
5977
+ * module of the project decides. Only pytest.param makes a row that
5978
+ * carries marks: a value written out, a class, a function, a name of
5979
+ * the module bound once to one of those is a plain row. A name, a
5980
+ * member or a call that is not read may be a marked one.
5981
+ */
5982
+ const rowState = (node, depth = 0) => {
5983
+ const value = unwrapped(node);
5984
+ if (!value || depth > 4) return 'sometimes';
5985
+ switch (value.type) {
5986
+ // The value of an operator is no row, whatever its operands are.
5987
+ case 'lambda': case 'comparison_operator': case 'not_operator': case 'unary_operator': case 'binary_operator':
5988
+ return RUN;
5989
+ case 'boolean_operator':
5990
+ return value.namedChildren.every(part => rowState(part, depth + 1) === RUN) ? RUN : 'sometimes';
5991
+ case 'conditional_expression': {
5992
+ const parts = value.namedChildren;
5993
+ return parts.length === 3 && rowState(parts[0], depth + 1) === RUN && rowState(parts[2], depth + 1) === RUN
5994
+ ? RUN : 'sometimes';
5995
+ }
5996
+ case 'identifier': return nameState(value.text, depth);
5997
+ case 'attribute': return memberState(value, depth);
5998
+ case 'call': return callState(value, depth);
5999
+ default: return PY_PLAIN_VALUES.has(value.type) ? RUN : 'sometimes';
6000
+ }
6001
+ };
6002
+ const writeState = (candidates, depth) => {
6003
+ if (candidates.length !== 1 || !candidates[0]) return 'sometimes';
6004
+ const write = candidates[0];
6005
+ if (write.kind === 'def' || write.kind === 'class') return RUN;
6006
+ return write.kind === 'assign' ? rowState(write.value, depth + 1) : 'sometimes';
6007
+ };
6008
+ const nameState = (name, depth) => {
6009
+ if (classWritten.has(name)) return 'sometimes';
6010
+ const candidates = candidatesOf(writesOf('module', name));
6011
+ const binding = imported.get(name);
6012
+ if (binding && candidates.length === 1 && candidates[0] && candidates[0].kind === 'import') {
6013
+ // A module is no row; a name of another module of the project is read there.
6014
+ if (binding.kind === 'import') return RUN;
6015
+ return inProject(binding.module) ? { via: { module: binding.module, name: binding.name } } : RUN;
6016
+ }
6017
+ // A name the module never binds is a builtin.
6018
+ if (candidates.length === 0) return stars.length === 0 && !binding ? RUN : 'sometimes';
6019
+ return writeState(candidates, depth);
6020
+ };
6021
+ const memberState = (node, depth) => {
6022
+ const written = dotted(node);
6023
+ if (!written) return 'sometimes';
6024
+ const parts = written.split('.');
6025
+ if (classWritten.has(parts[0])) return 'sometimes';
6026
+ const binding = imported.get(parts[0]);
6027
+ if (binding) {
6028
+ const path = pathOf(written);
6029
+ if (path && /^_?pytest(\.|$)/.test(path)) return 'sometimes';
6030
+ const held = candidatesOf(writesOf('module', parts[0]));
6031
+ if (held.length !== 1 || !held[0] || held[0].kind !== 'import') return 'sometimes';
6032
+ if (!inProject(binding.module)) return RUN;
6033
+ if (parts.length === 2 && binding.kind === 'import') return { via: { module: binding.module, name: parts[1] } };
6034
+ // A member of a class taken by name, or a name of a module taken from its package.
6035
+ if (parts.length === 2) return { via: { module: binding.module, name: binding.name, member: parts[1] } };
6036
+ return 'sometimes';
6037
+ }
6038
+ const candidates = candidatesOf(writesOf('module', parts[0]));
6039
+ if (candidates.length === 0) return stars.length === 0 ? RUN : 'sometimes';
6040
+ // A member of a class of the module, where the class body binds it.
6041
+ if (parts.length !== 2 || candidates.length !== 1 || !candidates[0] || candidates[0].kind !== 'class') return 'sometimes';
6042
+ const members = candidatesOf(writesOf(candidates[0].node.id, parts[1]));
6043
+ return members.length === 0 ? 'sometimes' : writeState(members, depth);
6044
+ };
6045
+ const argumentsOf = call => {
6046
+ const args = call.childForFieldName('arguments');
6047
+ return (args ? args.namedChildren : []).filter(argument => argument.type !== 'comment');
6048
+ };
6049
+ const callState = (call, depth) => {
6050
+ const callee = call.childForFieldName('function');
6051
+ const written = dotted(callee);
6052
+ const parts = written ? written.split('.') : [];
6053
+ const binding = written ? imported.get(parts[0]) : null;
6054
+ const path = binding ? pathOf(written) : null;
6055
+ const given = argumentsOf(call);
6056
+ // Calling what a call gave back: a row only where that could be one.
6057
+ if (callee && callee.type === 'call') {
6058
+ if (given.some(argument => argument.type === 'list_splat' || argument.type === 'dictionary_splat' ||
6059
+ carriesRows(argument.type === 'keyword_argument' ? argument.childForFieldName('value') : argument, null, depth + 1))) {
6060
+ return 'sometimes';
6061
+ }
6062
+ return callState(callee, depth + 1) === RUN ? RUN : 'sometimes';
6063
+ }
6064
+ if (path === 'pytest.param') {
6065
+ if (given.some(argument => argument.type === 'list_splat' || argument.type === 'dictionary_splat')) return 'sometimes';
6066
+ const marks = given.find(argument => argument.type === 'keyword_argument' &&
6067
+ argument.childForFieldName('name')?.text === 'marks');
6068
+ if (!marks) return RUN;
6069
+ const read = marksOfValue(unwrapped(marks.childForFieldName('value')));
6070
+ // pytest refuses a row that asks for fixtures.
6071
+ if (read.unread || read.fixturesOpen || read.marks.includes('usefixtures')) return 'sometimes';
6072
+ return read.skip === 'skip' ? 'skip' : read.skip === 'sometimes' ? 'sometimes' : RUN;
6073
+ }
6074
+ if (path && /^_?pytest(\.|$)/.test(path)) return 'sometimes';
6075
+ // What is handed in may come back out.
6076
+ if (given.some(argument => argument.type === 'list_splat' || argument.type === 'dictionary_splat' ||
6077
+ carriesRows(argument.type === 'keyword_argument' ? argument.childForFieldName('value') : argument, null, depth + 1))) {
6078
+ return 'sometimes';
6079
+ }
6080
+ if (!written || classWritten.has(parts[0])) return 'sometimes';
6081
+ if (binding) {
6082
+ const candidates = candidatesOf(writesOf('module', parts[0]));
6083
+ if (candidates.length !== 1 || !candidates[0] || candidates[0].kind !== 'import') return 'sometimes';
6084
+ // What a package's code gives back is not read.
6085
+ if (!inProject(binding.module)) return RUN;
6086
+ if (parts.length === 1 && binding.kind !== 'import') return { via: { module: binding.module, name: binding.name, call: true } };
6087
+ if (parts.length === 2 && binding.kind === 'import') return { via: { module: binding.module, name: parts[1], call: true } };
6088
+ if (parts.length === 2) return { via: { module: binding.module, name: binding.name, member: parts[1], call: true } };
6089
+ return 'sometimes';
6090
+ }
6091
+ const candidates = candidatesOf(writesOf('module', parts[0]));
6092
+ if (candidates.length === 0) return stars.length === 0 ? RUN : 'sometimes';
6093
+ if (parts.length !== 1 || candidates.length !== 1 || !candidates[0]) return 'sometimes';
6094
+ // An instance of a class of the module; what a function of the module gives back.
6095
+ if (candidates[0].kind === 'class') return RUN;
6096
+ return candidates[0].kind === 'def' && !makesRows(candidates[0].node, depth + 1) ? RUN : 'sometimes';
6097
+ };
6098
+ /**
6099
+ * Does an expression or a body hold anything that is a marked row or
6100
+ * may give one: pytest's param, a keyword `marks`, a name that is not
6101
+ * a plain row, a call that may give one back, the module's namespace.
6102
+ * `locals`: names of the function around it, which are what it was
6103
+ * handed.
6104
+ */
6105
+ const carriesRows = (node, locals, depth) => {
6106
+ if (!node) return false;
6107
+ if (depth > 6) return true;
6108
+ switch (node.type) {
6109
+ case 'keyword_argument':
6110
+ if (node.childForFieldName('name')?.text === 'marks') return true;
6111
+ return carriesRows(node.childForFieldName('value'), locals, depth);
6112
+ case 'identifier': {
6113
+ const name = node.text;
6114
+ if (name === 'param' || name === 'ParameterSet' || PY_NAMESPACE_NAMES.has(name)) return true;
6115
+ if (locals && locals.has(name)) return false;
6116
+ return nameState(name, depth + 1) !== RUN;
6117
+ }
6118
+ case 'attribute': {
6119
+ const written = dotted(node);
6120
+ if (!written) return carriesRows(node.childForFieldName('object'), locals, depth);
6121
+ const name = node.childForFieldName('attribute').text;
6122
+ if (name === 'param' || name === 'ParameterSet' || PY_NAMESPACE_NAMES.has(name)) return true;
6123
+ if (locals && locals.has(written.split('.')[0])) return false;
6124
+ return memberState(node, depth + 1) !== RUN;
6125
+ }
6126
+ case 'call': {
6127
+ const callee = node.childForFieldName('function');
6128
+ const written = dotted(callee);
6129
+ const root = written ? written.split('.')[0] : null;
6130
+ if (!(root && locals && locals.has(root)) && callState(node, depth + 1) !== RUN) return true;
6131
+ return argumentsOf(node).some(argument => carriesRows(argument, locals, depth));
6132
+ }
6133
+ default:
6134
+ return node.namedChildren.some(child => carriesRows(child, locals, depth));
6135
+ }
6136
+ };
6137
+ const makesMemo = new Map();
6138
+ /** Can a function of the module's top give back a marked row? */
6139
+ const makesRows = (fn, depth = 0) => {
6140
+ if (makesMemo.has(fn.id)) return makesMemo.get(fn.id);
6141
+ // A function that calls itself adds nothing by that.
6142
+ makesMemo.set(fn.id, false);
6143
+ const locals = new Set(parameterNames(fn));
6144
+ const body = fn.childForFieldName('body');
6145
+ // Names the body binds are what it computed from the rest.
6146
+ const bind = node => {
6147
+ if (node.type === 'assignment' || node.type === 'augmented_assignment' || node.type === 'named_expression') {
6148
+ for (const target of targetsOf(node.childForFieldName('left') || node.childForFieldName('name'))) locals.add(target.text);
6149
+ } else if (node.type === 'for_statement' || node.type === 'for_in_clause') {
6150
+ for (const target of targetsOf(node.childForFieldName('left'))) locals.add(target.text);
6151
+ }
6152
+ for (const child of node.namedChildren) bind(child);
6153
+ };
6154
+ if (body) bind(body);
6155
+ const answer = !body || carriesRows(body, locals, depth);
6156
+ makesMemo.set(fn.id, answer);
6157
+ return answer;
6158
+ };
6159
+ const NO_TABLE = Object.freeze({ skip: null });
6160
+ /**
6161
+ * What a table of parameters leaves of a test: nothing where one row
6162
+ * runs; `skip` ('skip': no row runs the body; 'sometimes': one may);
6163
+ * `table` where names of other modules of the project decide (`rest`:
6164
+ * what the other rows say).
6165
+ */
6166
+ const tableState = values => {
6167
+ const rows = [];
6168
+ let open = false; // a part that is not written out may be empty
6169
+ const add = (list, depth) => {
6170
+ for (const item of list.namedChildren) {
6171
+ if (item.type === 'comment') continue;
6172
+ if (item.type === 'list_splat') {
6173
+ const inner = depth < 4 ? sequenceOf(item.namedChildren[0]) : null;
6174
+ if (inner) add(inner, depth + 1);
6175
+ // The characters of a text written out are plain rows.
6176
+ else if (stringOf(item.namedChildren[0])) rows.push(RUN);
6177
+ else open = true;
6178
+ continue;
6179
+ }
6180
+ rows.push(rowState(item));
6181
+ }
6182
+ };
6183
+ add(values, 0);
6184
+ if (rows.some(state => state === RUN)) return NO_TABLE;
6185
+ if (rows.length === 0) return { skip: open ? 'sometimes' : 'skip' };
6186
+ const via = rows.filter(state => typeof state === 'object').map(state => state.via);
6187
+ const rest = !open && rows.every(state => typeof state === 'object' || state === 'skip') ? 'skip' : 'sometimes';
6188
+ return via.length === 0 ? { skip: rest } : { skip: null, table: { rest, via } };
6189
+ };
6190
+
6191
+ // ---- decorators and marks ----
6192
+ const NOTHING = Object.freeze({ skip: null, marks: [], open: false, fixtures: [], fixturesOpen: false });
6193
+ const UNREAD_MARK = Object.freeze({ skip: 'sometimes', marks: [], open: true, unread: true, fixtures: [], fixturesOpen: false });
6194
+ const keywordOf = (call, name) => {
6195
+ const args = call.childForFieldName('arguments');
6196
+ for (const argument of args ? args.namedChildren : []) {
6197
+ if (argument.type !== 'keyword_argument') continue;
6198
+ const key = argument.childForFieldName('name');
6199
+ if (key && key.text === name) return argument.childForFieldName('value');
6200
+ }
6201
+ return null;
6202
+ };
6203
+ const positional = call => {
6204
+ const args = call.childForFieldName('arguments');
6205
+ return (args ? args.namedChildren : []).filter(argument => argument.type !== 'keyword_argument' && argument.type !== 'comment');
6206
+ };
6207
+ const mentionsMarks = node => (node.type === 'keyword_argument' && node.childForFieldName('name')?.text === 'marks') ||
6208
+ node.namedChildren.some(mentionsMarks);
6209
+ const plainMark = (name, extra = {}) => ({ skip: null, marks: [name], open: false, fixtures: [], fixturesOpen: false, ...extra });
6210
+ /** What one decorator, or one value of pytestmark, is. */
6211
+ const readMark = (expression, depth = 0) => {
6212
+ if (!expression) return UNREAD_MARK;
6213
+ const call = expression.type === 'call' ? expression : null;
6214
+ const written = dotted(call ? call.childForFieldName('function') : expression);
6215
+ if (!written) {
6216
+ // pytest.mark.skip(reason=...)(function): a call of a call.
6217
+ return call && depth < 4 ? readMark(call.childForFieldName('function'), depth + 1) : UNREAD_MARK;
6218
+ }
6219
+ const path = imported.has(written.split('.')[0]) ? pathOf(written) : null;
6220
+ if (path) {
6221
+ const mark = /^pytest\.mark\.(\w+)$/.exec(path);
6222
+ if (mark) {
6223
+ if (mark[1] === 'skip') return plainMark('skip', { skip: 'skip' });
6224
+ if (mark[1] === 'skipif') return plainMark('skipif', { skip: 'sometimes' });
6225
+ if (mark[1] === 'xfail') {
6226
+ // run=False leaves the body out where the mark applies:
6227
+ // always without a condition, else where one is true.
6228
+ const run = call ? keywordOf(call, 'run') : null;
6229
+ const truth = run ? truthOf(run) : true;
6230
+ if (truth === true) return plainMark('xfail');
6231
+ const conditions = call ? [...positional(call), keywordOf(call, 'condition')].filter(Boolean) : [];
6232
+ // A condition written as text is evaluated by pytest.
6233
+ const held = conditions.map(condition => stringOf(condition) != null ? null : truthOf(condition));
6234
+ const applies = conditions.length === 0 || held.includes(true) ? true : held.every(each => each === false) ? false : null;
6235
+ return plainMark('xfail', {
6236
+ skip: applies === false ? null : truth === false && applies === true ? 'skip' : 'sometimes',
6237
+ });
6238
+ }
6239
+ if (mark[1] === 'parametrize') {
6240
+ // No value to run with: pytest skips the test. A list
6241
+ // that is not written out may be empty.
6242
+ // A row carries its own marks: one that runs the body
6243
+ // has to be in view.
6244
+ const given = call ? keywordOf(call, 'argvalues') || positional(call)[1] : null;
6245
+ const values = sequenceOf(given);
6246
+ const table = !call || !given || !values ? { skip: 'sometimes' } : tableState(values);
6247
+ return plainMark('parametrize', {
6248
+ skip: table.skip, open: !!call && mentionsMarks(call),
6249
+ ...(table.table && { tables: [table.table] }),
6250
+ });
6251
+ }
6252
+ if (mark[1] === 'usefixtures') {
6253
+ const names = call ? positional(call).map(stringOf) : [];
6254
+ return plainMark('usefixtures', {
6255
+ fixtures: names.filter(name => name != null),
6256
+ fixturesOpen: !call ? false : names.some(name => name == null),
6257
+ });
6258
+ }
6259
+ return plainMark(mark[1]);
6260
+ }
6261
+ if (PY_FIXTURE_PATHS.has(path)) return { ...NOTHING, fixture: true };
6262
+ if (path === 'unittest.skip') return { ...NOTHING, skip: 'skip' };
6263
+ if (path === 'unittest.skipIf' || path === 'unittest.skipUnless') return { ...NOTHING, skip: 'sometimes' };
6264
+ if (path === 'unittest.expectedFailure') return NOTHING;
6265
+ if (/^unittest\.mock\.patch(\.(object|dict|multiple))?$/.test(path)) return NOTHING;
6266
+ if (PY_ABSTRACT_PATHS.has(path) || path === 'functools.wraps' || path === 'typing.override' ||
6267
+ path === 'typing_extensions.override') return NOTHING;
6268
+ return UNREAD_MARK;
6269
+ }
6270
+ if ((written === 'staticmethod' || written === 'classmethod' || written === 'property') &&
6271
+ writesOf('module', written).length === 0) {
6272
+ return NOTHING;
6273
+ }
6274
+ // A name of this module bound once, to one of pytest's marks.
6275
+ const write = written.includes('.') ? null : settled('module', written);
6276
+ if (write && write.kind === 'assign' && depth < 4) {
6277
+ const inner = readMark(write.value, depth + 1);
6278
+ if (!inner.unread && !inner.fixture) return inner;
6279
+ }
6280
+ return UNREAD_MARK;
6281
+ };
6282
+ const combine = parts => ({
6283
+ skip: parts.some(part => part.skip === 'skip') ? 'skip' : parts.some(part => part.skip === 'sometimes') ? 'sometimes' : null,
6284
+ marks: [...new Set(parts.flatMap(part => part.marks))],
6285
+ open: parts.some(part => part.open),
6286
+ fixtures: [...new Set(parts.flatMap(part => part.fixtures || []))],
6287
+ fixturesOpen: parts.some(part => part.fixturesOpen),
6288
+ ...(parts.some(part => part.fixture) && { fixture: true }),
6289
+ ...(parts.some(part => part.tables) && { tables: parts.flatMap(part => part.tables || []) }),
6290
+ });
6291
+ const marksOfValue = node => {
6292
+ if (!node) return UNREAD_MARK;
6293
+ if (node.type === 'list' || node.type === 'tuple' || node.type === 'parenthesized_expression') {
6294
+ return combine(node.namedChildren.map(marksOfValue));
6295
+ }
6296
+ return readMark(node);
6297
+ };
6298
+ /** What pytestmark is in a scope once it has run. */
6299
+ const marksOf = scope => {
6300
+ const candidates = candidatesOf(writesOf(scope, 'pytestmark'));
6301
+ if (candidates.length === 0) return NOTHING;
6302
+ if (candidates.length === 1 && candidates[0] && candidates[0].kind === 'assign') return marksOfValue(candidates[0].value);
6303
+ const parts = candidates.map(write => write && write.kind === 'assign' ? marksOfValue(write.value) : NOTHING);
6304
+ return { ...combine(parts), skip: 'sometimes', open: true, fixturesOpen: true };
6305
+ };
6306
+ const decoratorsOf = info => {
6307
+ const holder = info && info.node.parent && info.node.parent.type === 'decorated_definition' ? info.node.parent : null;
6308
+ return (holder ? holder.namedChildren : []).filter(child => child.type === 'decorator').map(child => child.namedChildren[0]);
6309
+ };
6310
+ const decoratedMemo = new Map();
6311
+ const decoratedOf = info => {
6312
+ if (!info) return NOTHING;
6313
+ if (!decoratedMemo.has(info)) decoratedMemo.set(info, combine(decoratorsOf(info).map(expression => readMark(expression))));
6314
+ return decoratedMemo.get(info);
6315
+ };
6316
+ const settledModuleMarks = marksOf('module');
6317
+ const moduleMarks = marksUnsettled
6318
+ ? { ...settledModuleMarks, skip: settledModuleMarks.skip || 'sometimes', open: true, fixturesOpen: true }
6319
+ : settledModuleMarks;
6320
+
6321
+ // ---- code that can stop a test before its body ----
6322
+ const stopMemo = new Map();
6323
+ const NO_STOP = Object.freeze({ stops: false, calls: [] });
6324
+ /**
6325
+ * Can running a function stop a test: it mentions a stopping name,
6326
+ * raises, or calls a function of the module that can. `calls`: the
6327
+ * functions of other modules of the project it calls; whether one of
6328
+ * those can is said where the files are put together.
6329
+ */
6330
+ const stopFacts = (fn, raises = true) => {
6331
+ const key = `${fn.id}${raises ? '' : ' skips'}`;
6332
+ if (stopMemo.has(key)) return stopMemo.get(key);
6333
+ stopMemo.set(key, NO_STOP);
6334
+ const body = fn.childForFieldName('body');
6335
+ const calls = [];
6336
+ const answer = { stops: !!body && stops(body, calls, raises), calls };
6337
+ stopMemo.set(key, answer);
6338
+ return answer;
6339
+ };
6340
+ const mayStop = fn => stopFacts(fn).stops;
6341
+ // For a caller in another module: a function that names a skip. One
6342
+ // that only raises reports an error of its own, as any function may.
6343
+ const maySkip = fn => stopFacts(fn, false).stops;
6344
+ /** The function of another module of the project a callee names, or null. */
6345
+ const projectCall = callee => {
6346
+ const written = dotted(callee);
6347
+ if (!written) return null;
6348
+ const parts = written.split('.');
6349
+ const binding = imported.get(parts[0]);
6350
+ if (!binding) return null;
6351
+ if (binding.kind === 'import') {
6352
+ // module.function, by the module's own name or the one it is imported as
6353
+ if (parts.length !== 2 || !inProject(binding.module)) return null;
6354
+ return { module: binding.module, name: parts[1] };
6355
+ }
6356
+ // A function taken by name; or a module taken by name from its
6357
+ // package (from tests import helpers; helpers.need()).
6358
+ if (!inProject(binding.module)) return null;
6359
+ if (parts.length === 1) return { module: binding.module, name: binding.name };
6360
+ if (parts.length !== 2 || binding.kind !== 'from') return null;
6361
+ return { module: `${binding.module}.${binding.name}`, name: parts[1] };
6362
+ };
6363
+ const stops = (node, calls = [], raises = true) => {
6364
+ switch (node.type) {
6365
+ // A bare raise hands on what was already raised.
6366
+ case 'raise_statement':
6367
+ if (raises) return node.namedChildren.length > 0;
6368
+ break;
6369
+ case 'identifier': {
6370
+ if (PY_STOPPING_NAMES.has(node.text)) return true;
6371
+ const binding = imported.get(node.text);
6372
+ if (binding && binding.kind !== 'import' && PY_STOPPING_NAMES.has(binding.name)) return true;
6373
+ const path = pathOf(node.text);
6374
+ return !!path && PY_STOPPING_PATHS.test(path);
6375
+ }
6376
+ case 'attribute': {
6377
+ const written = dotted(node);
6378
+ const path = written ? pathOf(written) : null;
6379
+ // pytest.mark.skip is a mark: it is read where it is applied.
6380
+ if (path && /^pytest\.mark(\.|$)/.test(path)) return false;
6381
+ if (path && PY_STOPPING_PATHS.test(path)) return true;
6382
+ const name = node.childForFieldName('attribute');
6383
+ if (name && PY_STOPPING_NAMES.has(name.text)) return true;
6384
+ return stops(node.childForFieldName('object'), calls, raises);
6385
+ }
6386
+ case 'string_content': return PY_STOPPING_NAMES.has(node.text);
6387
+ case 'call': {
6388
+ const callee = node.childForFieldName('function');
6389
+ if (callee && callee.type === 'identifier' && !imported.has(callee.text)) {
6390
+ for (const fn of functionsNamed.get(callee.text) || []) {
6391
+ const facts = stopFacts(fn, raises);
6392
+ if (facts.stops) return true;
6393
+ calls.push(...facts.calls);
6394
+ }
6395
+ }
6396
+ const elsewhere = projectCall(callee);
6397
+ if (elsewhere) calls.push(elsewhere);
6398
+ break;
6399
+ }
6400
+ default: break;
6401
+ }
6402
+ return node.namedChildren.some(child => stops(child, calls, raises));
6403
+ };
6404
+ /**
6405
+ * Can a hook change what is collected or run: it can stop a test, it
6406
+ * yields (it wraps the step), it returns a value where the value
6407
+ * decides, or it uses what pytest hands it in a way that is none of
6408
+ * the calls that only read.
6409
+ */
6410
+ const hookDecides = fn => {
6411
+ if (mayStop(fn)) return true;
6412
+ const body = fn.childForFieldName('body');
6413
+ if (!body) return true;
6414
+ const hookName = fn.childForFieldName('name').text;
6415
+ const wraps = node => node.type === 'yield' ||
6416
+ (node.type === 'return_statement' && !PYTEST_REPORTING_HOOKS.has(hookName) &&
6417
+ !!node.namedChildren[0] && node.namedChildren[0].type !== 'none') ||
6418
+ node.namedChildren.some(wraps);
6419
+ if (wraps(body)) return true;
6420
+ const handed = new Set(parameterNames(fn));
6421
+ if (handed.size === 0) return false;
6422
+ const isReference = node => {
6423
+ const parent = node.parent;
6424
+ if (!parent) return true;
6425
+ if (parent.type === 'attribute' && sameNode(parent.childForFieldName('attribute'), node)) return false;
6426
+ if (parent.type === 'keyword_argument' && sameNode(parent.childForFieldName('name'), node)) return false;
6427
+ // The name bound again: nothing is done to what it held.
6428
+ if (parent.type === 'assignment' && sameNode(parent.childForFieldName('left'), node)) return false;
6429
+ return true;
6430
+ };
6431
+ // What one use of a handed object is: false when it can change
6432
+ // something, else what the use makes of it.
6433
+ const useOf = reference => {
6434
+ let top = reference;
6435
+ for (;;) {
6436
+ while (top.parent && top.parent.type === 'attribute' && sameNode(top.parent.childForFieldName('object'), top)) top = top.parent;
6437
+ const call = top.parent;
6438
+ if (top.type !== 'attribute' || !call || call.type !== 'call' || !sameNode(call.childForFieldName('function'), top)) return false;
6439
+ const method = top.childForFieldName('attribute').text;
6440
+ if (!PYTEST_HOOK_READS.has(method)) return false;
6441
+ if (method === 'addinivalue_line') {
6442
+ const key = stringOf(positional(call)[0]);
6443
+ if (key !== 'markers' && key !== 'filterwarnings') return false;
6444
+ }
6445
+ if (!PYTEST_HOOK_OBJECTS.has(method)) return {};
6446
+ const holder = call.parent;
6447
+ if (holder && holder.type === 'attribute' && sameNode(holder.childForFieldName('object'), call)) {
6448
+ top = call;
6449
+ continue;
6450
+ }
6451
+ if (holder && holder.type === 'assignment' && sameNode(holder.childForFieldName('right'), call)) {
6452
+ const left = holder.childForFieldName('left');
6453
+ return left && left.type === 'identifier' ? { hands: left.text } : false;
6454
+ }
6455
+ return holder && PY_TRUTH_TESTS.has(holder.type) ? {} : false;
6456
+ }
6457
+ };
6458
+ let decides = false;
6459
+ let grew = true;
6460
+ const visit = node => {
6461
+ if (decides) return;
6462
+ if (node.type === 'identifier' && handed.has(node.text) && isReference(node)) {
6463
+ const use = useOf(node);
6464
+ if (use === false) decides = true;
6465
+ else if (use.hands && !handed.has(use.hands)) {
6466
+ handed.add(use.hands);
6467
+ grew = true;
6468
+ }
6469
+ return;
6470
+ }
6471
+ for (const child of node.namedChildren) visit(child);
6472
+ };
6473
+ while (grew && !decides) {
6474
+ grew = false;
6475
+ visit(body);
6476
+ }
6477
+ return decides;
6478
+ };
6479
+ /** The fixture a definition is, or null: its name, whether every test gets it, whether it can stop one. */
6480
+ const fixtureOf = info => {
6481
+ if (!info || info.node.type !== 'function_definition') return null;
6482
+ let made = null;
6483
+ for (const expression of decoratorsOf(info)) {
6484
+ const call = expression.type === 'call' ? expression : null;
6485
+ const written = dotted(call ? call.childForFieldName('function') : expression);
6486
+ const path = written ? pathOf(written) : null;
6487
+ if (path && PY_FIXTURE_PATHS.has(path)) made = { call };
6488
+ }
6489
+ if (!made) return null;
6490
+ const call = made.call;
6491
+ const functionName = info.node.childForFieldName('name').text;
6492
+ const renamed = call ? keywordOf(call, 'name') : null;
6493
+ const autouse = call ? keywordOf(call, 'autouse') : null;
6494
+ const params = call ? keywordOf(call, 'params') : null;
6495
+ const values = params ? sequenceOf(params) : null;
6496
+ // No row that runs the body in view: a list that is not written
6497
+ // out, an empty one, rows whose own marks skip.
6498
+ const table = !params ? NO_TABLE : !values ? { skip: 'sometimes' } : tableState(values);
6499
+ return {
6500
+ name: renamed ? stringOf(renamed) : functionName,
6501
+ function: functionName,
6502
+ // A value that is not written out may be true.
6503
+ autouse: !!autouse && truthOf(autouse) !== false,
6504
+ decides: mayStop(info.node) || table.skip != null || (!!renamed && stringOf(renamed) == null),
6505
+ ...(table.table && { tables: [table.table] }),
6506
+ // A test that asks for it runs once for every parameter.
6507
+ ...(params && { params: true }),
6508
+ ...(stopFacts(info.node).calls.length > 0 && { calls: stopFacts(info.node).calls }),
6509
+ requests: parameterNames(info.node),
6510
+ };
6511
+ };
6512
+
6513
+ // ---- what runs when the module is imported ----
6514
+ let moduleSkips = false;
6515
+ let namespaceWritten = false;
6516
+ const importTimeCalls = []; // functions of other modules of the project called when this one is imported
6517
+ const isMainTest = condition => !!condition && condition.type === 'comparison_operator' &&
6518
+ condition.namedChildren.length === 2 && condition.children.some(child => child.type === '==') &&
6519
+ condition.namedChildren.some(child => child.type === 'identifier' && child.text === '__name__') &&
6520
+ condition.namedChildren.some(child => stringOf(child) === '__main__');
6521
+ const importTime = node => {
6522
+ switch (node.type) {
6523
+ // Bodies run when called; decorators are read with the definition.
6524
+ case 'function_definition': case 'lambda': case 'import_statement': case 'import_from_statement':
6525
+ case 'decorator':
6526
+ return;
6527
+ case 'class_definition': {
6528
+ const body = node.childForFieldName('body');
6529
+ for (const child of body ? body.namedChildren : []) importTime(child);
6530
+ return;
6531
+ }
6532
+ case 'if_statement':
6533
+ // pytest never imports a module under the name __main__.
6534
+ if (isMainTest(node.childForFieldName('condition'))) {
6535
+ for (const child of node.namedChildren) {
6536
+ if (child.type === 'elif_clause' || child.type === 'else_clause') importTime(child);
6537
+ }
6538
+ return;
6539
+ }
6540
+ break;
6541
+ case 'raise_statement':
6542
+ if (node.namedChildren.length > 0) moduleSkips = true;
6543
+ return;
6544
+ case 'identifier': {
6545
+ if (PY_NAMESPACE_NAMES.has(node.text)) namespaceWritten = true;
6546
+ const path = pathOf(node.text);
6547
+ if (path && PY_STOPPING_PATHS.test(path)) moduleSkips = true;
6548
+ return;
6549
+ }
6550
+ case 'attribute': {
6551
+ const written = dotted(node);
6552
+ const path = written ? pathOf(written) : null;
6553
+ if (path && /^pytest\.mark(\.|$)/.test(path)) return;
6554
+ if (path && PY_STOPPING_PATHS.test(path)) {
6555
+ moduleSkips = true;
6556
+ return;
6557
+ }
6558
+ const name = node.childForFieldName('attribute');
6559
+ if (name && PY_NAMESPACE_NAMES.has(name.text)) namespaceWritten = true;
6560
+ importTime(node.childForFieldName('object'));
6561
+ return;
6562
+ }
6563
+ case 'call': {
6564
+ const callee = node.childForFieldName('function');
6565
+ if (callee && callee.type === 'identifier' && !imported.has(callee.text)) {
6566
+ for (const fn of functionsNamed.get(callee.text) || []) {
6567
+ if (mayStop(fn)) moduleSkips = true;
6568
+ importTimeCalls.push(...stopFacts(fn).calls);
6569
+ }
6570
+ }
6571
+ const elsewhere = projectCall(callee);
6572
+ if (elsewhere) importTimeCalls.push(elsewhere);
6573
+ break;
6574
+ }
6575
+ default: break;
6576
+ }
6577
+ for (const child of node.namedChildren) importTime(child);
6578
+ };
6579
+ importTime(root);
6580
+
6581
+ // ---- what a definition is once its scope has run ----
6582
+ const infoOf = symbol => definitions.get((symbol.nameLine || symbol.startLine) - 1) ||
6583
+ definitions.get(symbol.startLine - 1) || null;
6584
+ const mentions = (node, name) => !!node && ((node.type === 'identifier' && node.text === name) ||
6585
+ node.namedChildren.some(child => mentions(child, name)));
6586
+ // 'definite': the name holds this definition. 'maybe': it may (a
6587
+ // branch, a later write that wraps it). 'gone': it does not.
6588
+ const stateOf = (info, name) => {
6589
+ if (!info) return 'gone';
6590
+ const candidates = candidatesOf(writesOf(info.scope, name));
6591
+ const mine = candidates.some(write => write && write.pos === info.pos);
6592
+ if (mine) return candidates.length === 1 ? 'definite' : 'maybe';
6593
+ return candidates.every(write => write && write.kind === 'assign' && mentions(write.value, name)) ? 'maybe' : 'gone';
6594
+ };
6595
+ /**
6596
+ * A function as a test, by what it is itself: its definition, its
6597
+ * decorators, a switch written on it. Null: no test (gone, a fixture).
6598
+ * One switched off stays (`off`): the switch is the state of an object
6599
+ * that another module may hold under another name and write again.
6600
+ */
6601
+ const rowOf = symbol => {
6602
+ const info = infoOf(symbol);
6603
+ const state = stateOf(info, symbol.name);
6604
+ if (state === 'gone') return null;
6605
+ const own = decoratedOf(info);
6606
+ if (own.fixture) return null;
6607
+ const written = attributeSwitch(info.scope, symbol.name);
6608
+ const dynamic = state === 'maybe' ? 'defined-conditionally'
6609
+ : (switchUnsettled || (written && written.on === 'unknown')) ? 'collection-switch' : null;
6610
+ return {
6611
+ name: symbol.name, line: symbol.nameLine || symbol.startLine, startLine: symbol.startLine,
6612
+ endLine: symbol.endLine ?? symbol.startLine, sourceSpan: symbol.sourceSpan,
6613
+ named: functionNamed(symbol.name), prefixed: symbol.name.startsWith('test'),
6614
+ // (Where a switch of the file is not read, off is not settled either.)
6615
+ ...(written && written.on === false && !switchUnsettled && { off: true }),
6616
+ ...(written && written.forced && { forced: written.forced }),
6617
+ ...(own.tables && { tables: own.tables }),
6618
+ ...(own.skip === 'skip' && { skipped: true }),
6619
+ ...((own.skip === 'sometimes' || dynamic) && { conditional: true }),
6620
+ ...(dynamic && { dynamic }),
6621
+ marks: own.marks, marksOpen: own.open,
6622
+ requests: [...new Set([...parameterNames(info.node), ...own.fixtures])],
6623
+ ...(own.fixturesOpen && { requestsOpen: true }),
6624
+ };
6625
+ };
6626
+ /** Is a name bound to something this does not list that could be a test? */
6627
+ const boundToMore = writes => candidatesOf(writes).some(write => write && (write.kind === 'other' || write.kind === 'augment' ||
6628
+ (write.kind === 'assign' && !PY_PLAIN_VALUES.has(unwrapped(write.value).type))));
6629
+ let more = namespaceWritten || switchUnsettled;
6630
+
6631
+ // ---- the module's own functions, other names for them, lambdas ----
6632
+ const moduleSwitch = switchUnsettled ? OPEN_SWITCH : switchValue(writesOf('module', '__test__'));
6633
+ const moduleOff = !!moduleSwitch && moduleSwitch.on === false;
6634
+ const units = [];
6635
+ const functions = [];
6636
+ const imports = [];
6637
+ for (const symbol of symbols) {
6638
+ if (!helpers.isCallable(symbol) || symbol.type === 'constructor') continue;
6639
+ if (helpers.enclosing(symbol).length > 0) continue;
6640
+ const info = infoOf(symbol);
6641
+ if (!info || info.scope !== 'module') continue;
6642
+ const row = rowOf(symbol);
6643
+ if (!row) continue;
6644
+ functions.push(row);
6645
+ if ((row.named || row.forced) && !row.off) units.push({ kind: 'test', suite: [], ...row });
6646
+ }
6647
+ for (const [name, writes] of scopeWrites.get('module') || []) {
6648
+ const write = settled('module', name);
6649
+ const value = write && write.kind === 'assign' ? unwrapped(write.value) : null;
6650
+ if (value && value.type === 'identifier' && value.text !== name) {
6651
+ // Another name for something the module holds.
6652
+ imports.push({ name, original: value.text, module: null, line: write.line });
6653
+ continue;
6654
+ }
6655
+ if (value && value.type === 'lambda' && !functions.some(other => other.name === name)) {
6656
+ const row = {
6657
+ name, line: write.line, startLine: write.line, endLine: write.endLine,
6658
+ named: functionNamed(name), prefixed: name.startsWith('test'), marks: [], marksOpen: false,
6659
+ requests: parameterNames(value),
6660
+ };
6661
+ functions.push(row);
6662
+ if (row.named) units.push({ kind: 'test', suite: [], ...row });
6663
+ continue;
6664
+ }
6665
+ if ((functionNamed(name) || classNamed(name)) && boundToMore(writes)) more = true;
6666
+ }
6667
+ // Names taken from other modules that the module still holds when it
6668
+ // has run: a test class or function among them is collected here.
6669
+ for (const binding of fileEntry.importBindings || []) {
6670
+ if (binding.kind === 'import') continue;
6671
+ const bound = binding.alias || binding.name;
6672
+ const write = settled('module', bound);
6673
+ if (!write || write.kind !== 'import' || write.line !== binding.line) continue;
6674
+ imports.push({ name: bound, original: binding.name, module: binding.module, line: binding.line });
6675
+ }
6676
+
6677
+ // ---- classes ----
6678
+ const classInfo = cls => infoOf(cls);
6679
+ const classes = [];
6680
+ const eligibleMemo = new Map();
6681
+ // Its definition is there, and so is every class around it.
6682
+ const eligible = cls => {
6683
+ if (!eligibleMemo.has(cls)) {
6684
+ eligibleMemo.set(cls, false);
6685
+ eligibleMemo.set(cls, stateOf(classInfo(cls), cls.name) !== 'gone' &&
6686
+ helpers.enclosing(cls).every(outer => outer.type === 'class' && eligible(outer)));
6687
+ }
6688
+ return eligibleMemo.get(cls);
6689
+ };
6690
+ for (const cls of symbols) {
6691
+ if (cls.type !== 'class') continue;
6692
+ const chain = helpers.enclosing(cls);
6693
+ if (chain.some(outer => outer.type !== 'class') || !eligible(cls)) continue;
6694
+ const info = classInfo(cls);
6695
+ const scope = info.node.id;
6696
+ // A base is looked up by the name it was declared under.
6697
+ const written = basesOf(cls).map(base => ({ base, from: origin(base) }));
6698
+ const declaredHere = name => symbols.some(symbol => symbol.type === 'class' && symbol.name === name) ||
6699
+ writesOf('module', name).length > 0;
6700
+ // A class of unittest, or (taken as one) a class from outside the
6701
+ // project that is named like one: neither brings a test.
6702
+ const outsideCase = ({ base, from }) => /TestCase$/.test(from.name) &&
6703
+ (from.bound ? !inProject(from.module) : !declaredHere(base.split('.')[0]));
6704
+ const ownMarks = combine([decoratedOf(info), marksOf(scope)]);
6705
+ const ownSwitch = switchUnsettled ? OPEN_SWITCH
6706
+ : switchValue([...writesOf(scope, '__test__'), ...(attributeWrites.get(`${info.scope}\0${cls.name}`) || [])]);
6707
+ const superclasses = info.node.childForFieldName('superclasses');
6708
+ const metaclass = (superclasses ? superclasses.namedChildren : [])
6709
+ .find(argument => argument.type === 'keyword_argument' && argument.childForFieldName('name')?.text === 'metaclass');
6710
+ const metaPath = metaclass ? pathOf(dotted(metaclass.childForFieldName('value')) || '') : null;
6711
+ const members = new Set(symbols.filter(symbol => holds(cls, symbol)).map(symbol => symbol.name));
6712
+ for (const name of (scopeWrites.get(scope) || new Map()).keys()) members.add(name);
6713
+ const tests = [];
6714
+ const abstract = [];
6715
+ const fixtures = [];
6716
+ const guardCalls = [];
6717
+ let guard = false;
6718
+ let generates = false;
6719
+ // What the class body leaves under a constructor's name: 'custom'
6720
+ // (a function of its own: pytest refuses the class), 'object' (the
6721
+ // builtin's, written back), 'unknown'. Undefined: the body binds
6722
+ // none and the next class in the lookup order decides.
6723
+ const constructorOf = name => {
6724
+ if (info.scope === 'module' && constructorWrites.has(cls.name)) return 'unknown';
6725
+ const kinds = candidatesOf(writesOf(scope, name)).map(write => {
6726
+ if (write === null || write.kind === 'del') return 'absent';
6727
+ if (write.kind === 'def') return 'custom';
6728
+ const value = write.kind === 'assign' ? dotted(unwrapped(write.value)) : null;
6729
+ return value === `object.${name}` && !imported.has('object') && writesOf('module', 'object').length === 0 &&
6730
+ stars.length === 0 ? 'object' : 'unknown';
6731
+ });
6732
+ if (kinds.every(kind => kind === 'absent')) return undefined;
6733
+ return kinds.every(kind => kind === kinds[0]) ? kinds[0] : 'unknown';
6734
+ };
6735
+ const constructs = {};
6736
+ for (const name of PYTHON_CONSTRUCTORS) {
6737
+ const kind = constructorOf(name);
6738
+ if (kind) constructs[name] = kind;
6739
+ }
6740
+ // typing.Protocol gives every class that lists it a constructor
6741
+ // of its own (measured: pytest refuses the class and what derives from it).
6742
+ if (written.some(entry => /^Protocol(\[|$)/.test(entry.from.name) &&
6743
+ (entry.from.bound ? /^typing(_extensions)?$/.test(entry.from.module) : !declaredHere(entry.base.split('.')[0].replace(/\[.*$/, ''))))) {
6744
+ constructs.__init__ = 'custom';
6745
+ }
6746
+ for (const symbol of symbols) {
6747
+ if (!holds(cls, symbol) || !helpers.isCallable(symbol)) continue;
6748
+ const memberInfo = infoOf(symbol);
6749
+ if (!memberInfo || memberInfo.scope !== scope) continue;
6750
+ // pytest calls a class's own pytest_generate_tests for its tests.
6751
+ if (symbol.name === 'pytest_generate_tests' && stateOf(memberInfo, symbol.name) !== 'gone') generates = true;
6752
+ if (decoratorsOf(memberInfo).some(expression => {
6753
+ const path = pathOf(dotted(expression.type === 'call' ? expression.childForFieldName('function') : expression) || '');
6754
+ return !!path && PY_ABSTRACT_PATHS.has(path);
6755
+ })) abstract.push(symbol.name);
6756
+ const fixture = fixtureOf(memberInfo);
6757
+ if (fixture) {
6758
+ fixtures.push(fixture);
6759
+ if (fixture.autouse && fixture.decides) guard = true;
6760
+ if (fixture.autouse) guardCalls.push(...(fixture.calls || []));
6761
+ continue;
6762
+ }
6763
+ if (PY_CLASS_SETUPS.has(symbol.name) && stateOf(memberInfo, symbol.name) !== 'gone') {
6764
+ if (mayStop(memberInfo.node)) guard = true;
6765
+ guardCalls.push(...stopFacts(memberInfo.node).calls);
6766
+ }
6767
+ if (symbol.type === 'constructor') continue;
6768
+ const row = rowOf(symbol);
6769
+ if (!row) continue;
6770
+ // A member switched off is no test of the class or of one
6771
+ // deriving from it (`off`): it stays, for its name hides the
6772
+ // one further up and a module that takes the class may write
6773
+ // the switch again.
6774
+ if (row.named || row.prefixed || row.forced || symbol.name === 'runTest') tests.push(row);
6775
+ }
6776
+ // Another name for a test of the class is one more test.
6777
+ for (const [name, writes] of scopeWrites.get(scope) || []) {
6778
+ const write = settled(scope, name);
6779
+ const value = write && write.kind === 'assign' ? unwrapped(write.value) : null;
6780
+ const target = value && value.type === 'identifier' && value.text !== name
6781
+ ? tests.find(row => row.name === value.text && !row.alias && !row.off) : null;
6782
+ if (target && settled(scope, value.text) && settled(scope, value.text).kind === 'def') {
6783
+ tests.push({
6784
+ ...target, name, line: write.line, of: target.line, alias: true,
6785
+ named: functionNamed(name), prefixed: name.startsWith('test'),
6786
+ });
6787
+ } else if ((functionNamed(name) || name.startsWith('test') || classNamed(name)) && boundToMore(writes)) {
6788
+ more = true;
6789
+ }
6790
+ }
6791
+ classes.push({
6792
+ name: cls.name, suite: chain.map(outer => outer.name), bases: written.map(entry => entry.from.name),
6793
+ // Bases known to bring neither a constructor nor a test.
6794
+ plainBases: written.filter(entry => PYTHON_PLAIN_BASES.has(entry.from.name) ||
6795
+ entry.from.module === 'unittest' || (entry.from.module === null && /^unittest\./.test(entry.base)) ||
6796
+ outsideCase(entry)).map(entry => entry.from.name),
6797
+ named: classNamed(cls.name),
6798
+ ...(written.some(entry => (entry.from.module === 'unittest' && /TestCase$/.test(entry.from.name)) ||
6799
+ (entry.from.module === null && /^unittest\.\w*TestCase$/.test(entry.base)) || outsideCase(entry)) && { unittestBase: true }),
6800
+ // pytest does not collect a class whose constructor, by its
6801
+ // lookup order, is not object's; unittest's classes are built
6802
+ // by unittest.
6803
+ ...(Object.keys(constructs).length > 0 && { constructs }),
6804
+ ...(generates && { generates: true }),
6805
+ ...(ownMarks.tables && { tables: ownMarks.tables }),
6806
+ ...(ownSwitch && { enabled: ownSwitch.on }),
6807
+ ...(ownSwitch && ownSwitch.forced && { forced: ownSwitch.forced }),
6808
+ ...(stateOf(info, cls.name) === 'maybe' && { unsettled: true }),
6809
+ ...(ownMarks.skip && { skip: ownMarks.skip }),
6810
+ ...(guard && { guard: true }),
6811
+ ...(guardCalls.length > 0 && { guardCalls }),
6812
+ ...(fixtures.length > 0 && { fixtures }),
6813
+ ...(ownMarks.fixtures.length > 0 && { usefixtures: ownMarks.fixtures }),
6814
+ ...(ownMarks.fixturesOpen && { usefixturesOpen: true }),
6815
+ ...(abstract.length > 0 && { abstract }),
6816
+ ...((written.some(entry => entry.from.module === 'abc' && entry.from.name === 'ABC') || metaPath === 'abc.ABCMeta')
6817
+ ? { abc: true } : metaclass ? { abc: 'unknown' } : {}),
6818
+ marks: ownMarks.marks, marksOpen: ownMarks.open,
6819
+ line: cls.nameLine || cls.startLine, startLine: cls.startLine, endLine: cls.endLine ?? cls.startLine, sourceSpan: cls.sourceSpan,
6820
+ members: [...members], tests,
6821
+ });
6822
+ }
6823
+
6824
+ // ---- what the file does as a plugin (a conftest.py, a module named by pytest_plugins) ----
6825
+ const moduleFixtures = [];
6826
+ const moduleGuardCalls = [];
6827
+ const hookCalls = []; // functions of other modules of the project its hooks call
6828
+ const stopping = []; // functions of the module's top that name a skip: they stop a test of a module that calls them
6829
+ let hooksDecide = false;
6830
+ let hooksEverywhere = false;
6831
+ let generates = false;
6832
+ let moduleGuard = false;
6833
+ for (const info of definitions.values()) {
6834
+ if (info.scope !== 'module' || info.node.type !== 'function_definition') continue;
6835
+ const name = info.node.childForFieldName('name').text;
6836
+ if (stateOf(info, name) === 'gone') continue;
6837
+ if (maySkip(info.node)) stopping.push(name);
6838
+ const fixture = fixtureOf(info);
6839
+ if (fixture) {
6840
+ moduleFixtures.push(fixture);
6841
+ if (fixture.autouse) moduleGuardCalls.push(...(fixture.calls || []));
6842
+ continue;
6843
+ }
6844
+ if (/^pytest_/.test(name) && hookDecides(info.node)) {
6845
+ hooksDecide = true;
6846
+ if (!PYTEST_NODE_HOOKS.has(name)) hooksEverywhere = true;
6847
+ // The one hook pytest also calls in a test module.
6848
+ if (name === 'pytest_generate_tests') generates = true;
6849
+ } else if (/^pytest_/.test(name)) {
6850
+ // A hook that decides nothing itself may call a function of
6851
+ // another module of the project that does.
6852
+ for (const call of stopFacts(info.node).calls) {
6853
+ hookCalls.push({
6854
+ ...call,
6855
+ ...(!PYTEST_NODE_HOOKS.has(name) && { everywhere: true }),
6856
+ ...(name === 'pytest_generate_tests' && { generates: true }),
6857
+ });
6858
+ }
6859
+ }
6860
+ if (PY_MODULE_SETUPS.has(name)) {
6861
+ if (mayStop(info.node)) moduleGuard = true;
6862
+ moduleGuardCalls.push(...stopFacts(info.node).calls);
6863
+ }
6864
+ }
6865
+ const plugins = stars.map(star => ({ module: star.module, by: 'import' }));
6866
+ for (const [name, writes] of scopeWrites.get('module') || []) {
6867
+ if (!/^pytest_/.test(name)) continue;
6868
+ if (name === 'pytest_plugins') {
6869
+ // The modules named are loaded as plugins.
6870
+ const write = settled('module', name);
6871
+ const value = write && write.kind === 'assign' ? unwrapped(write.value) : null;
6872
+ const names = !value ? null : stringOf(value) != null ? [stringOf(value)]
6873
+ : (value.type === 'list' || value.type === 'tuple')
6874
+ ? value.namedChildren.filter(item => item.type !== 'comment').map(stringOf) : null;
6875
+ if (!names || names.some(each => each == null)) {
6876
+ hooksDecide = true;
6877
+ hooksEverywhere = true;
6878
+ } else {
6879
+ for (const each of names) plugins.push({ module: each, by: 'name' });
6880
+ }
6881
+ continue;
6882
+ }
6883
+ // A hook bound by anything but its own definition is not read.
6884
+ if (writes.some(write => write.kind !== 'def')) {
6885
+ hooksDecide = true;
6886
+ if (!PYTEST_NODE_HOOKS.has(name)) hooksEverywhere = true;
6887
+ if (name === 'pytest_generate_tests') generates = true;
6888
+ }
6889
+ }
6890
+ // What a star import brings: the names __all__ lists when it is
6891
+ // written out, every public name otherwise.
6892
+ const allWrites = writesOf('module', '__all__');
6893
+ const allWrite = settled('module', '__all__');
6894
+ const allValue = allWrite && allWrite.kind === 'assign' ? unwrapped(allWrite.value) : null;
6895
+ const allNames = allValue && (allValue.type === 'list' || allValue.type === 'tuple')
6896
+ ? allValue.namedChildren.filter(item => item.type !== 'comment').map(stringOf) : null;
6897
+ const exportsAll = allWrites.length === 0 ? null
6898
+ : allNames && allNames.every(name => name != null) && allWrites.length === 1 ? allNames : 'unknown';
6899
+
6900
+ const takenConstructors = [...constructorWrites].filter(name => imported.has(name) || starBound('module', name));
6901
+ /**
6902
+ * What a name of this module is as a row of parameters, for a module
6903
+ * that takes it: `member` of a class the name holds, `call` for what
6904
+ * calling it gives back. Null: the module does not bind the name.
6905
+ * { via }: it is what a name of another module is, to be read there;
6906
+ * { stars }: one of the modules this one takes every name of binds it.
6907
+ */
6908
+ const rowNamed = (name, member = null, call = false, depth = 0) => {
6909
+ if (depth > 4 || classWritten.has(name)) return 'sometimes';
6910
+ if (writesOf('module', name).length === 0) {
6911
+ if (namespaceWritten) return 'sometimes';
6912
+ return stars.length > 0 ? { stars: stars.map(star => star.module) } : null;
6913
+ }
6914
+ const top = settled('module', name);
6915
+ if (!top) return 'sometimes';
6916
+ if (top.kind === 'import') {
6917
+ const binding = imported.get(name);
6918
+ if (!binding) return 'sometimes';
6919
+ // A module taken whole is a plain row; a name of it is read in it.
6920
+ if (binding.kind === 'import') {
6921
+ return member ? { via: { module: binding.module, name: member, call } } : call ? 'sometimes' : RUN;
6922
+ }
6923
+ return { via: { module: binding.module, name: binding.name, ...(member && { member }), call } };
6924
+ }
6925
+ if (top.kind === 'assign') {
6926
+ // Another name for something of the module is what that is.
6927
+ const value = unwrapped(top.value);
6928
+ if (value && value.type === 'identifier' && value.text !== name) return rowNamed(value.text, member, call, depth + 1);
6929
+ return member || call ? 'sometimes' : rowState(top.value, 1);
6930
+ }
6931
+ if (member) {
6932
+ if (top.kind !== 'class' || call) return 'sometimes';
6933
+ const members = candidatesOf(writesOf(top.node.id, member));
6934
+ return members.length === 0 ? 'sometimes' : writeState(members, 0);
6935
+ }
6936
+ if (top.kind === 'class') return RUN;
6937
+ if (top.kind === 'def') return call && makesRows(top.node) ? 'sometimes' : RUN;
6938
+ return 'sometimes';
6939
+ };
6940
+ const collected = collection.collected !== false;
6941
+ return {
6942
+ derived: true,
6943
+ rowNamed,
6944
+ // A switch written in a way that is not read: which of the names
6945
+ // the module holds it turned on or off is not settled.
6946
+ ...(switchUnsettled && { switchesOpen: true }),
6947
+ units: collected ? units : [],
6948
+ classes, functions, imports, stars, exportsAll, linearization: 'c3',
6949
+ // The members whose lookup decides whether pytest takes a class.
6950
+ constructors: PYTHON_CONSTRUCTORS,
6951
+ module: {
6952
+ ...(moduleMarks.skip || moduleSkips ? { skip: moduleMarks.skip === 'skip' ? 'skip' : 'sometimes' } : {}),
6953
+ ...(moduleSwitch && moduleSwitch.on === 'unknown' && { unsettled: true }),
6954
+ ...((moduleGuard || moduleFixtures.some(fixture => fixture.autouse && fixture.decides) || generates) && { guard: true }),
6955
+ ...(moduleMarks.fixtures.length > 0 && { usefixtures: moduleMarks.fixtures }),
6956
+ ...(moduleMarks.fixturesOpen && { usefixturesOpen: true }),
6957
+ // Functions of other modules of the project called when the
6958
+ // module is imported, and by its setups: one that can stop a
6959
+ // test makes its tests conditional.
6960
+ ...(importTimeCalls.length > 0 && { skipCalls: importTimeCalls }),
6961
+ ...(moduleGuardCalls.length > 0 && { guardCalls: moduleGuardCalls }),
6962
+ // Switches written here on names taken from other modules.
6963
+ switches: Object.fromEntries([...attributeWrites.keys()].filter(key => key.startsWith('module\0'))
6964
+ .map(key => key.slice('module\0'.length))
6965
+ .filter(name => starBound('module', name) ||
6966
+ (writesOf('module', name).length === 1 && writesOf('module', name)[0].kind === 'import'))
6967
+ .map(name => [name, switchUnsettled ? OPEN_SWITCH : attributeSwitch('module', name)])),
6968
+ // Names taken from other modules whose constructor is written here.
6969
+ ...(takenConstructors.length > 0 && { constructorWrites: takenConstructors }),
6970
+ ...(moduleMarks.tables && { tables: moduleMarks.tables }),
6971
+ },
6972
+ runtime: {
6973
+ decides: hooksDecide, everywhere: hooksEverywhere, generates, fixtures: moduleFixtures, plugins,
6974
+ ...(hookCalls.length > 0 && { hookCalls }),
6975
+ },
6976
+ stopping: [...new Set(stopping)],
6977
+ marks: moduleMarks.marks, marksOpen: moduleMarks.open,
6978
+ ...(moduleOff && { off: true }),
6979
+ ...(more && collected && { inheriting: true }),
6980
+ ...(!collected && { notCollected: true }),
6981
+ };
6982
+ }
6983
+
5488
6984
  /**
5489
6985
  * Classify a Python symbol as a runtime entry point of a specific kind.
5490
6986
  * Returns 'test' | 'framework' | null.
@@ -5847,7 +7343,215 @@ function annotatedReceiverType(tree, line, column, receiver) {
5847
7343
  return typeNameFromAnnotation(parameter.childForFieldName('type')) || undefined;
5848
7344
  }
5849
7345
 
7346
+ // The comments the type checker reads as directives (pyright's tokenizer).
7347
+ const TYPE_IGNORE = /(^|#)\s*type:\s*ignore(\[[\s\w-,]*\]|\s|$)/;
7348
+ const PYRIGHT_IGNORE = /(^|#)\s*pyright:\s*ignore(?:\[([\s\w-,]*)\]|\s|$)/;
7349
+ const CHECK_MODES = new Set(['basic', 'standard', 'strict']);
7350
+
7351
+ function checkSettingOperands(text) {
7352
+ const value = text.slice(1).trim();
7353
+ if (!value.startsWith('pyright:')) return null;
7354
+ const operands = value.slice('pyright:'.length);
7355
+ return operands.trim().startsWith('ignore') ? null : operands;
7356
+ }
7357
+
7358
+ /** Use the same ignore grammar for suppression and changed-code context. */
7359
+ function checkDirectiveComment(text) {
7360
+ // Ordinary type annotations also affect checking. Keep those even
7361
+ // though they are not suppressions read by checkSuppressedAt.
7362
+ return /^#\s*(?:type|pyright)\s*:/.test(text) ||
7363
+ TYPE_IGNORE.test(text) || PYRIGHT_IGNORE.test(text);
7364
+ }
7365
+
7366
+ function checkFileDirectiveComment(text) {
7367
+ if (TYPE_IGNORE.test(text)) return 'leading-or-local';
7368
+ if (PYRIGHT_IGNORE.test(text)) return null;
7369
+ return checkSettingOperands(text) !== null ? 'column-zero-file' : null;
7370
+ }
7371
+ // The rule each kind of break is reported under.
7372
+ const RULE_OF = Object.freeze({
7373
+ 'arity': 'reportCallIssue',
7374
+ 'import-name': 'reportAttributeAccessIssue',
7375
+ 'import-module': 'reportMissingImports',
7376
+ 'undefined-name': 'reportUndefinedVariable',
7377
+ });
7378
+
7379
+ /**
7380
+ * What the comments of a file tell the checker:
7381
+ * all a `type: ignore` comment before any token: nothing is reported;
7382
+ * lines line -> null (everything on it is ignored: `type: ignore`,
7383
+ * `pyright: ignore`) or the rules a `pyright: ignore[..]` names;
7384
+ * levels rule -> level set by `# pyright: rule=level` comments. The
7385
+ * checker applies them even at an invalid position, reporting
7386
+ * a separate placement diagnostic; changed-code context tracks
7387
+ * that column-zero requirement independently.
7388
+ * `basic`, `standard` and `strict` choose a rule set: none of
7389
+ * them turns a rule off.
7390
+ */
7391
+ function checkDirectives(root) {
7392
+ const ignored = new Map();
7393
+ const levels = new Map();
7394
+ let all = false;
7395
+ const firstToken = root.children.find(child => child.type !== 'comment');
7396
+ const codeStart = firstToken ? firstToken.startIndex : Infinity;
7397
+ for (const comment of root.descendantsOfType('comment')) {
7398
+ const text = comment.text;
7399
+ const line = comment.startPosition.row + 1;
7400
+ if (TYPE_IGNORE.test(text)) {
7401
+ if (comment.startIndex < codeStart) all = true;
7402
+ else ignored.set(line, null);
7403
+ }
7404
+ const scoped = PYRIGHT_IGNORE.exec(text);
7405
+ if (scoped && !(ignored.has(line) && ignored.get(line) === null)) {
7406
+ ignored.set(line, scoped[2] === undefined ? null
7407
+ : scoped[2].split(',').map(rule => rule.trim()).filter(Boolean));
7408
+ }
7409
+ const operands = checkSettingOperands(text);
7410
+ if (operands === null) continue;
7411
+ const list = operands.split(',').map(operand => operand.trim()).filter(Boolean);
7412
+ if (list.some(operand => CHECK_MODES.has(operand))) levels.clear();
7413
+ for (const operand of list) {
7414
+ const [rule, level] = operand.split('=').map(part => part.trim());
7415
+ // A level the checker does not know leaves the rule as it was.
7416
+ if (rule && CHECK_LEVELS.has(level)) levels.set(rule, level);
7417
+ }
7418
+ }
7419
+ return { all, ignored, levels };
7420
+ }
7421
+
7422
+ // The levels a `# pyright: rule=level` comment may set.
7423
+ const CHECK_LEVELS = new Set(['true', 'false', 'error', 'warning', 'information', 'none']);
7424
+
7425
+ /** The `call` node that covers exactly `span`, or null. */
7426
+ function callAtSpan(root, span) {
7427
+ for (let node = root.descendantForIndex(span.start, Math.max(span.start, span.end - 1)); node; node = node.parent) {
7428
+ if (node.startIndex === span.start && node.endIndex === span.end && node.type === 'call') return node;
7429
+ if (node.startIndex < span.start || node.endIndex > span.end) break;
7430
+ }
7431
+ return null;
7432
+ }
7433
+
7434
+ /**
7435
+ * The ranges the checker reports a break over, one per report it makes: a
7436
+ * comment on any line of a range silences that report, and the break is
7437
+ * silenced when every report is.
7438
+ * arity too few arguments: the whole call. One positional
7439
+ * argument too many, a keyword the callable does not
7440
+ * take or takes twice: that argument;
7441
+ * import-name the imported name; import-module: the module name;
7442
+ * undefined-name the line itself.
7443
+ * The call is the one the engine measured (`site.span`), whatever name it
7444
+ * is called under and however many calls share its line; an import is the
7445
+ * statement that names `site.module`.
7446
+ */
7447
+ function reportedRanges(root, site) {
7448
+ const line = site.line;
7449
+ const range = [{ row: line - 1, column: 0 }, { row: line, column: 0 }];
7450
+ const rows = node => [node.startPosition.row + 1, node.endPosition.row + 1];
7451
+ const spans = node => node.startPosition.row + 1 <= line && node.endPosition.row + 1 >= line;
7452
+ if (site.kind === 'arity') {
7453
+ const nameOf = call => {
7454
+ const callee = call.childForFieldName('function');
7455
+ return callee ? (callee.childForFieldName('attribute') || callee) : null;
7456
+ };
7457
+ const call = site.span ? callAtSpan(root, site.span)
7458
+ : root.descendantsOfType('call', ...range).filter(candidate => {
7459
+ const named = nameOf(candidate);
7460
+ return !!named && named.startPosition.row + 1 === line;
7461
+ }).find((candidate, at, all) => !site.name || nameOf(candidate).text === site.name ||
7462
+ !all.some(other => nameOf(other).text === site.name));
7463
+ if (!call) return [[line, line]];
7464
+ const given = (call.childForFieldName('arguments')?.namedChildren || [])
7465
+ .filter(argument => argument.type !== 'comment');
7466
+ const keyword = name => given.find(argument => argument.type === 'keyword_argument' &&
7467
+ argument.childForFieldName('name')?.text === name);
7468
+ const reports = [];
7469
+ // What the call was measured against, problem by problem.
7470
+ for (const problem of String(site.problem || '').split('; ').filter(Boolean)) {
7471
+ const named = /'([^']+)'/.exec(problem);
7472
+ if (/^takes \d+ positional/.test(problem)) {
7473
+ const positional = given.filter(argument => argument.type !== 'keyword_argument');
7474
+ const count = Number(/^takes (\d+)/.exec(problem)[1]);
7475
+ reports.push(rows(positional[count] || call));
7476
+ } else if (named && !/^missing/.test(problem) && keyword(named[1])) {
7477
+ reports.push(rows(keyword(named[1])));
7478
+ } else {
7479
+ reports.push(rows(call));
7480
+ }
7481
+ }
7482
+ if (reports.length > 0) return reports;
7483
+ if (Number.isInteger(site.most) && given.length > site.most) return [rows(given[site.most])];
7484
+ return [rows(call)];
7485
+ }
7486
+ if (site.kind === 'import-name' || site.kind === 'import-module') {
7487
+ const statements = root.descendantsOfType(['import_from_statement', 'import_statement'], ...range).filter(spans);
7488
+ const moduleOf = statement => statement.childForFieldName('module_name') ||
7489
+ statement.namedChildren.find(child => child.type === 'dotted_name' || child.type === 'aliased_import');
7490
+ const naming = site.module === undefined ? statements
7491
+ : statements.filter(statement => statement.descendantsOfType(['dotted_name', 'relative_import'])
7492
+ .some(node => node.text === site.module));
7493
+ for (const statement of naming.length > 0 ? naming : statements) {
7494
+ if (site.kind === 'import-name') {
7495
+ const named = statement.descendantsOfType(['dotted_name', 'identifier'])
7496
+ .find(node => node.text === site.name);
7497
+ if (named) return [rows(named)];
7498
+ }
7499
+ const module = moduleOf(statement);
7500
+ if (module) return [rows(module)];
7501
+ }
7502
+ }
7503
+ return [[line, line]];
7504
+ }
7505
+
7506
+ /**
7507
+ * Has the type checker been told not to report this break? As pyright
7508
+ * reads its comments: a `type: ignore` before any code, the break's rule
7509
+ * set to anything but an error by a `# pyright:` comment, or a
7510
+ * `type: ignore` / `pyright: ignore` comment (naming the rule, when it
7511
+ * names any) on a line of every range it is reported over.
7512
+ *
7513
+ * @param {object} site - { lines, root, line, kind, name, most, span,
7514
+ * problem, module, memo }
7515
+ */
7516
+ function checkSuppressedAt(site) {
7517
+ const { root, memo } = site;
7518
+ if (!root) return false;
7519
+ if (memo && !memo.has('directives')) memo.set('directives', checkDirectives(root));
7520
+ const directives = memo ? memo.get('directives') : checkDirectives(root);
7521
+ if (directives.all) return true;
7522
+ const rule = RULE_OF[site.kind] || null;
7523
+ const level = rule ? directives.levels.get(rule) : undefined;
7524
+ if (level !== undefined && level !== 'true' && level !== 'error') return true;
7525
+ const silenced = ([first, last]) => {
7526
+ for (let at = first; at <= last; at++) {
7527
+ if (!directives.ignored.has(at)) continue;
7528
+ const rules = directives.ignored.get(at);
7529
+ if (rules === null || (rule !== null && rules.includes(rule))) return true;
7530
+ }
7531
+ return false;
7532
+ };
7533
+ return reportedRanges(root, site).every(silenced);
7534
+ }
7535
+
7536
+ /**
7537
+ * Is the import written on `line` a statement the type checker resolves?
7538
+ * `importlib.import_module(..)` and `__import__(..)` are calls to it.
7539
+ * Several imports can share a line: the one asked about is the one that
7540
+ * names `module`, as it is written.
7541
+ */
7542
+ function checkedImportAt(root, line, module) {
7543
+ const range = [{ row: line - 1, column: 0 }, { row: line, column: 0 }];
7544
+ return root.descendantsOfType(['import_statement', 'import_from_statement', 'future_import_statement'], ...range)
7545
+ .some(node => node.startPosition.row + 1 <= line && node.endPosition.row + 1 >= line &&
7546
+ (module === undefined || node.descendantsOfType(['dotted_name', 'relative_import'])
7547
+ .some(named => named.text === module)));
7548
+ }
7549
+
5850
7550
  module.exports = {
7551
+ checkDirectiveComment,
7552
+ checkFileDirectiveComment,
7553
+ checkSuppressedAt,
7554
+ checkedImportAt,
5851
7555
  annotatedReceiverType,
5852
7556
  findScopeAlternativeBindings,
5853
7557
  findPythonModuleEvidence,
@@ -5865,5 +7569,6 @@ module.exports = {
5865
7569
  findPytestFunctions,
5866
7570
  isEntryPoint,
5867
7571
  getEntryPointKind,
7572
+ testUnits,
5868
7573
  parse
5869
7574
  };