bmad-module-skill-forge 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +3 -3
- package/docs/_data/pinned.yaml +19 -13
- package/docs/architecture.md +2 -2
- package/docs/concepts.md +4 -4
- package/docs/examples.md +32 -25
- package/docs/getting-started.md +3 -3
- package/docs/how-it-works.md +2 -2
- package/docs/index.md +1 -1
- package/docs/skill-model.md +41 -39
- package/docs/verifying-a-skill.md +41 -20
- package/docs/why-skf.md +1 -1
- package/docs/workflows.md +2 -2
- package/package.json +1 -1
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +11 -3
- package/src/shared/scripts/schemas/skill-brief.v1.json +1 -1
- package/src/shared/scripts/skf-build-change-manifest.py +881 -168
- package/src/shared/scripts/skf-count-tokens.py +54 -10
- package/src/shared/scripts/skf-detect-workspaces.py +67 -0
- package/src/shared/scripts/skf-emit-result-envelope.py +6 -5
- package/src/shared/scripts/skf-extract-public-api.py +439 -107
- package/src/shared/scripts/skf-extraction-inventory.py +10 -6
- package/src/shared/scripts/skf-extraction-snapshot.py +6 -6
- package/src/shared/scripts/skf-parse-gaps.py +135 -26
- package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
- package/src/shared/scripts/skf-resolve-customization.py +197 -0
- package/src/shared/scripts/skf-shard-body.py +239 -41
- package/src/shared/scripts/skf-validate-output.py +12 -3
- package/src/shared/scripts/skf-verify-provenance-completeness.py +470 -38
- package/src/shared/tool-requirements.yaml +8 -2
- package/src/skf-analyze-source/SKILL.md +2 -2
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-audit-skill/SKILL.md +2 -2
- package/src/skf-audit-skill/references/re-index.md +2 -2
- package/src/skf-brief-skill/SKILL.md +2 -2
- package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -2
- package/src/skf-brief-skill/references/scope-definition.md +4 -4
- package/src/skf-campaign/SKILL.md +2 -2
- package/src/skf-campaign/references/step-05-skill-loop.md +6 -1
- package/src/skf-create-skill/SKILL.md +2 -2
- package/src/skf-create-skill/assets/skill-sections.md +1 -1
- package/src/skf-create-skill/references/extract.md +2 -1
- package/src/skf-create-skill/references/extraction-patterns.md +3 -3
- package/src/skf-create-skill/references/report.md +6 -1
- package/src/skf-create-stack-skill/SKILL.md +2 -2
- package/src/skf-create-stack-skill/references/validate.md +1 -1
- package/src/skf-drop-skill/SKILL.md +2 -2
- package/src/skf-export-skill/SKILL.md +2 -2
- package/src/skf-export-skill/assets/snippet-format.md +5 -5
- package/src/skf-export-skill/references/generate-snippet.md +22 -15
- package/src/skf-export-skill/references/load-skill.md +1 -1
- package/src/skf-export-skill/references/token-report.md +29 -9
- package/src/skf-forger/SKILL.md +2 -2
- package/src/skf-quick-skill/SKILL.md +2 -2
- package/src/skf-refine-architecture/SKILL.md +6 -8
- package/src/skf-rename-skill/SKILL.md +2 -2
- package/src/skf-setup/SKILL.md +2 -2
- package/src/skf-test-skill/SKILL.md +2 -2
- package/src/skf-test-skill/assets/output-section-formats.md +6 -4
- package/src/skf-test-skill/references/coherence-check.md +4 -3
- package/src/skf-test-skill/references/coverage-check-branches.md +61 -0
- package/src/skf-test-skill/references/coverage-check-tiers.md +1 -1
- package/src/skf-test-skill/references/coverage-check.md +45 -69
- package/src/skf-test-skill/references/init.md +2 -1
- package/src/skf-test-skill/references/migration-section-rules.md +26 -14
- package/src/skf-test-skill/references/report.md +2 -2
- package/src/skf-test-skill/references/scoring-rules.md +5 -4
- package/src/skf-test-skill/references/source-access-protocol.md +6 -6
- package/src/skf-test-skill/scripts/gap-ledger.py +378 -163
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +171 -70
- package/src/skf-test-skill/scripts/locate-export-segments.py +101 -20
- package/src/skf-test-skill/scripts/reconcile-coverage.py +192 -39
- package/src/skf-test-skill/scripts/score-signatures.py +329 -54
- package/src/skf-test-skill/scripts/stage-helper-payload.py +64 -13
- package/src/skf-test-skill/scripts/validate-inventory.py +108 -9
- package/src/skf-test-skill/templates/test-report-template.md +1 -0
- package/src/skf-update-skill/SKILL.md +2 -2
- package/src/skf-update-skill/references/detect-changes.md +1 -1
- package/src/skf-update-skill/references/gap-driven.md +36 -21
- package/src/skf-update-skill/references/merge.md +15 -10
- package/src/skf-update-skill/references/re-extract.md +3 -1
- package/src/skf-update-skill/references/report.md +5 -4
- package/src/skf-update-skill/references/write.md +39 -18
- package/src/skf-verify-stack/SKILL.md +2 -2
- package/tools/cli/lib/tool-check.js +158 -2
|
@@ -89,7 +89,19 @@ name (`App.update`) it is the last segment. The rules follow the
|
|
|
89
89
|
a keyword argument or a parameter never matches; a file that does not
|
|
90
90
|
tokenize is matched line by line. A `from X import *` line counts only
|
|
91
91
|
when no other line of the file defines NAME (a re-export the
|
|
92
|
-
extraction could not trace is cited at its star import).
|
|
92
|
+
extraction could not trace is cited at its star import). An
|
|
93
|
+
`@overload` group, where every line found is a `def` / `async def
|
|
94
|
+
NAME` line at one indentation in one enclosing block (one class, not
|
|
95
|
+
two) and every one but at most the last has `@overload` or
|
|
96
|
+
`@<module>.overload` (`@typing.overload`,
|
|
97
|
+
`@typing_extensions.overload`, `@t.overload` after
|
|
98
|
+
`import typing as t`) in its contiguous decorator block, defines NAME
|
|
99
|
+
at each of its lines, the stubs and the implementation. A comment line
|
|
100
|
+
may sit in the decorator block, and so may a decorator's arguments on
|
|
101
|
+
lines of their own, unless the file does not tokenize. A recorded line
|
|
102
|
+
that misses the group gives its first line alone, which is its first
|
|
103
|
+
stub: the public-API runner, its merge rule and the structural diff
|
|
104
|
+
all record that line, so `fix` can move the entry there.
|
|
93
105
|
- TS/JS (`.ts`, `.tsx`, `.mts`, `.cts`, `.js`, `.jsx`, `.mjs`, `.cjs`): a
|
|
94
106
|
`function` (or `function*`), `class`, `interface`, `type`, `enum`,
|
|
95
107
|
`namespace`, `const`, `let` or `var` declaration of NAME, with any of
|
|
@@ -98,7 +110,11 @@ name (`App.update`) it is the last segment. The rules follow the
|
|
|
98
110
|
and `class`; a name inside a decorator's arguments is not declared);
|
|
99
111
|
a later declarator of a `const`, `let` or `var` list
|
|
100
112
|
(`export const a = 1, NAME = 2;`, or `export const a = 1,` then
|
|
101
|
-
` NAME = 2;` as Prettier writes it: see Declarator lists below);
|
|
113
|
+
` NAME = 2;` as Prettier writes it: see Declarator lists below);
|
|
114
|
+
NAME bound by a destructuring pattern that begins such a list
|
|
115
|
+
(`export const { a, b: NAME } = obj;`, `const [NAME] = list`, the
|
|
116
|
+
pattern on one line or several), on the line where NAME appears (see
|
|
117
|
+
Declarator lists below); NAME
|
|
102
118
|
on the line right after a line that ends in its declaration keyword
|
|
103
119
|
(`export function`, `export interface`, `export const` and so on),
|
|
104
120
|
which is the line cited;
|
|
@@ -125,14 +141,23 @@ name (`App.update`) it is the last segment. The rules follow the
|
|
|
125
141
|
Declarator lists: a `const`, `let` or `var` list is split at its
|
|
126
142
|
top-level commas, outside strings, regular expression literals and
|
|
127
143
|
brackets, a type-argument list included (`Map<K, V>`: a `<` right after
|
|
128
|
-
a name), up to a top-level
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
counts. A `<` where an expression begins (JSX, a type assertion, a generic
|
|
144
|
+
a name), up to a top-level `;`. The list goes on to the next line when a
|
|
145
|
+
line ends inside a bracket, after a top-level comma or after an operator
|
|
146
|
+
waiting for its operand, or when the next line starts with an operator (a
|
|
147
|
+
member chain or a ternary broken over lines), so a declarator on a line
|
|
148
|
+
of its own counts. A `<` where an expression begins (JSX, a type assertion, a generic
|
|
134
149
|
arrow function's type parameters) is not followed: no later declarator of
|
|
135
|
-
the list counts after it.
|
|
150
|
+
the list counts after it. A destructuring pattern that begins the list,
|
|
151
|
+
on the keyword's line, declares the names it binds: a shorthand key, the
|
|
152
|
+
target after `key:`, an array element, a rest `...NAME` and the names of
|
|
153
|
+
a nested pattern, each on the line where it appears (the pattern may
|
|
154
|
+
span lines), with its statement's column. A renamed or computed key, a
|
|
155
|
+
default value, the type annotation after the pattern and anything inside
|
|
156
|
+
a string or a comment bind nothing, and neither does a pattern that does
|
|
157
|
+
not close. The later declarators after a pattern count as any others. A
|
|
158
|
+
pattern that is a later declarator (`export const a = 1, { b } = c`) or
|
|
159
|
+
that sits on the line after a split keyword (`export const`, then
|
|
160
|
+
` { b } = c;`) is not covered: it declares nothing.
|
|
136
161
|
|
|
137
162
|
Indentation: for a dotted name every match counts. For an undotted name,
|
|
138
163
|
matches at any indentation count, but when the file has a column-0 match
|
|
@@ -140,7 +165,8 @@ the indented ones are dropped, so a local shadow never outranks the
|
|
|
140
165
|
module-level definition. A line inside a multi-line import, export list
|
|
141
166
|
or `module.exports` object takes the column of the line its statement
|
|
142
167
|
starts on, and so do the name line of a declaration split after its
|
|
143
|
-
keyword
|
|
168
|
+
keyword, a later declarator on a line of its own and the line of a name
|
|
169
|
+
a destructuring pattern binds. When the recorded
|
|
144
170
|
line is itself an indented match that this rule dropped (a method
|
|
145
171
|
recorded under an undotted name beside a module-level function of the
|
|
146
172
|
same name), the finding lists the recorded line too, so it carries
|
|
@@ -149,7 +175,11 @@ several lines and no caller auto-moves it.
|
|
|
149
175
|
Entries in other files, and entries whose `export_type` is `module` or
|
|
150
176
|
`package` (case-insensitive; neither has a definition line), are skipped
|
|
151
177
|
and counted in `summary.line_check_skipped`. A `line-not-definition` item
|
|
152
|
-
lists every defining line in the file in `definition_lines
|
|
178
|
+
lists every defining line in the file in `definition_lines`, the recorded
|
|
179
|
+
line included when it is an indented match the column-0 rule dropped (see
|
|
180
|
+
Indentation). The one exception is a Python `@overload` group: it lists
|
|
181
|
+
the group's first line alone, unless the recorded line is such a dropped
|
|
182
|
+
indented match, which still gives several lines. An empty list
|
|
153
183
|
means the rules found no definition line, which may be a shape they do not
|
|
154
184
|
cover: the entry is unverified, not gone, and no caller auto-fixes it.
|
|
155
185
|
|
|
@@ -387,7 +417,8 @@ Subcommands:
|
|
|
387
417
|
}
|
|
388
418
|
|
|
389
419
|
With `--line`, a recorded line that is an indented match the column-0
|
|
390
|
-
rule dropped joins `definition_lines`,
|
|
420
|
+
rule dropped joins `definition_lines`, and one that misses a Python
|
|
421
|
+
`@overload` group gives its first line alone, as in `verify`.
|
|
391
422
|
|
|
392
423
|
classify-stale --names <coverage.json> --provenance <provenance-map.json>
|
|
393
424
|
--source-root <path> [--inventory <inventory.json>]
|
|
@@ -450,8 +481,9 @@ Subcommands:
|
|
|
450
481
|
`export default NAME`, an export-list item without `as`, a
|
|
451
482
|
`module.exports` shorthand key, a binding whose value is an import
|
|
452
483
|
(`require(...)` or `(await) import(...)`, on its line or the next:
|
|
453
|
-
`const NAME = require(...)`, `
|
|
454
|
-
`{ NAME: require(...) }`), an
|
|
484
|
+
`const NAME = require(...)`, `const { NAME } = require(...)`,
|
|
485
|
+
`exports.NAME = require(...)`, `{ NAME: require(...) }`), an
|
|
486
|
+
indented line and any line of a Python
|
|
455
487
|
file that does not tokenize never count. A module or package named
|
|
456
488
|
NAME (`NAME.py`, `NAME/__init__.py`, a TS/JS `NAME.<ext>` or
|
|
457
489
|
`NAME/index.<ext>`) declares NAME too: at its line 1 when no line of it
|
|
@@ -599,6 +631,7 @@ Exit codes:
|
|
|
599
631
|
from __future__ import annotations
|
|
600
632
|
|
|
601
633
|
import argparse
|
|
634
|
+
import bisect
|
|
602
635
|
import copy
|
|
603
636
|
import importlib.util
|
|
604
637
|
import io
|
|
@@ -913,6 +946,10 @@ _PY_SPLIT_DECL_RE = re.compile(r"^\s*(?:async\s+def|def|class)\s*\\$")
|
|
|
913
946
|
# A line that can matter to a name it does not hold: an import (which may go
|
|
914
947
|
# on over the next lines) or a `def \` / `class \` whose name is on the next.
|
|
915
948
|
_PY_NAMELESS_MATCH_RE = re.compile(r"^\s*(?:from|import|async|def|class)\b")
|
|
949
|
+
# The decorator of an `@overload` stub, imported by name or used through its
|
|
950
|
+
# module under any name (`typing`, `typing_extensions`, `t` after
|
|
951
|
+
# `import typing as t`).
|
|
952
|
+
_PY_OVERLOAD_RE = re.compile(r"^\s*@\s*(?:[A-Za-z_]\w*\.)?overload\s*$")
|
|
916
953
|
# Tokens that never begin a statement.
|
|
917
954
|
_PY_LAYOUT_TOKENS = frozenset(
|
|
918
955
|
{
|
|
@@ -989,6 +1026,11 @@ _DECL_MAYBE = "maybe"
|
|
|
989
1026
|
# What `_js_declarators` gives: None, or (brackets left open, how) the list
|
|
990
1027
|
# goes on with, and the pieces that begin a declarator.
|
|
991
1028
|
_DeclSplit = tuple[tuple[str, str] | None, list[str]]
|
|
1029
|
+
# What `_js_destructured` gives for a `const` / `let` / `var` statement
|
|
1030
|
+
# whose first declarator is a destructuring pattern: the (name, line) of
|
|
1031
|
+
# each name it binds, and the (line, text after the `=` on that line) its
|
|
1032
|
+
# initializer starts at, None when it has none.
|
|
1033
|
+
_PatternDecl = tuple[list[tuple[str, int]], tuple[int, str] | None]
|
|
992
1034
|
# After one of these, `/` begins a regular expression and `<` begins JSX, a
|
|
993
1035
|
# type assertion or a generic arrow function's type parameters.
|
|
994
1036
|
_JS_EXPRESSION_START = frozenset("([{,;=?:&|^%+-*/<>!~")
|
|
@@ -1105,15 +1147,17 @@ def _js_code_lines(lines: list[str]) -> list[str]:
|
|
|
1105
1147
|
|
|
1106
1148
|
|
|
1107
1149
|
class _SourceCache:
|
|
1108
|
-
"""Source lines, Python statement starts
|
|
1109
|
-
|
|
1110
|
-
|
|
1150
|
+
"""Source lines, Python statement starts, TS/JS comment-free lines and
|
|
1151
|
+
the names TS/JS destructuring patterns bind, read once per file, and
|
|
1152
|
+
TS/JS declarator lists, split once per line (neither depends on the
|
|
1153
|
+
export looked up)."""
|
|
1111
1154
|
|
|
1112
1155
|
def __init__(self) -> None:
|
|
1113
1156
|
self._lines: dict[Path, list[str] | None] = {}
|
|
1114
1157
|
self._starts: dict[Path, set[int] | None] = {}
|
|
1115
1158
|
self._js_code: dict[Path, list[str]] = {}
|
|
1116
1159
|
self._js_quiet: dict[Path, list[bool]] = {}
|
|
1160
|
+
self._js_patterns: dict[Path, dict[int, _PatternDecl]] = {}
|
|
1117
1161
|
self._js_decl: dict[tuple[str, str, bool], _DeclSplit] = {}
|
|
1118
1162
|
|
|
1119
1163
|
def lines(self, path: Path) -> list[str] | None:
|
|
@@ -1136,6 +1180,13 @@ class _SourceCache:
|
|
|
1136
1180
|
self._js_quiet[path] = _js_quiet_lines(code_lines)
|
|
1137
1181
|
return self._js_quiet[path]
|
|
1138
1182
|
|
|
1183
|
+
def js_destructured(
|
|
1184
|
+
self, path: Path, code_lines: list[str]
|
|
1185
|
+
) -> dict[int, _PatternDecl]:
|
|
1186
|
+
if path not in self._js_patterns:
|
|
1187
|
+
self._js_patterns[path] = _js_destructured(code_lines)
|
|
1188
|
+
return self._js_patterns[path]
|
|
1189
|
+
|
|
1139
1190
|
def js_declarators(
|
|
1140
1191
|
self, text: str, opened: str = "", fresh: bool = True
|
|
1141
1192
|
) -> _DeclSplit:
|
|
@@ -1269,6 +1320,76 @@ def _python_definition_lines(
|
|
|
1269
1320
|
return found or star_imports
|
|
1270
1321
|
|
|
1271
1322
|
|
|
1323
|
+
def _py_overloaded(lines: list[str], number: int, starts: set[int] | None) -> bool:
|
|
1324
|
+
"""True when the `def` on line `number` has `@overload` or
|
|
1325
|
+
`@<module>.overload` (`@typing.overload`, `@t.overload`) in its
|
|
1326
|
+
contiguous decorator block: the decorator lines at its indentation
|
|
1327
|
+
right above it, up to a blank line or a line that is not a decorator.
|
|
1328
|
+
A comment line is passed over, and so, in a file that tokenizes, is a
|
|
1329
|
+
line on which no statement begins (a decorator's arguments over
|
|
1330
|
+
several lines)."""
|
|
1331
|
+
text = lines[number - 1]
|
|
1332
|
+
indent = text[: len(text) - len(text.lstrip())]
|
|
1333
|
+
i = number - 1
|
|
1334
|
+
while i >= 1:
|
|
1335
|
+
text = lines[i - 1]
|
|
1336
|
+
code = text.split("#", 1)[0]
|
|
1337
|
+
if not text.strip():
|
|
1338
|
+
return False
|
|
1339
|
+
if not code.strip() or (starts is not None and i not in starts):
|
|
1340
|
+
i -= 1
|
|
1341
|
+
continue
|
|
1342
|
+
if not code.lstrip().startswith("@") or text[: len(text) - len(text.lstrip())] != indent:
|
|
1343
|
+
return False
|
|
1344
|
+
if _PY_OVERLOAD_RE.match(code):
|
|
1345
|
+
return True
|
|
1346
|
+
i -= 1
|
|
1347
|
+
return False
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
def _py_enclosing_line(
|
|
1351
|
+
lines: list[str], number: int, indent: str, starts: set[int] | None
|
|
1352
|
+
) -> int | None:
|
|
1353
|
+
"""The line of the block that holds the statement on line `number`,
|
|
1354
|
+
indented by `indent`: the nearest line above it where a statement
|
|
1355
|
+
begins with less indentation. None at module level."""
|
|
1356
|
+
i = number - 1
|
|
1357
|
+
while indent and i >= 1:
|
|
1358
|
+
text = lines[i - 1]
|
|
1359
|
+
if text.split("#", 1)[0].strip() and (starts is None or i in starts):
|
|
1360
|
+
if len(text) - len(text.lstrip()) < len(indent):
|
|
1361
|
+
return i
|
|
1362
|
+
i -= 1
|
|
1363
|
+
return None
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
def _python_overload_start(
|
|
1367
|
+
lines: list[str], name: str, defs: list[int], starts: set[int] | None
|
|
1368
|
+
) -> int | None:
|
|
1369
|
+
"""The first of `defs` when they are one `@overload` group: two lines
|
|
1370
|
+
or more, each a `def` / `async def NAME` line, all at one indentation
|
|
1371
|
+
in one enclosing block (`_py_enclosing_line`, so two classes'
|
|
1372
|
+
methods never make one group), and each but at most the last
|
|
1373
|
+
decorated as an overload (`_py_overloaded`). None otherwise."""
|
|
1374
|
+
if len(defs) < 2:
|
|
1375
|
+
return None
|
|
1376
|
+
decl = re.compile(rf"^(\s*)(?:async\s+def|def)\s+{re.escape(name)}(?!\w)")
|
|
1377
|
+
indents = set()
|
|
1378
|
+
for number in defs:
|
|
1379
|
+
m = decl.match(lines[number - 1].split("#", 1)[0])
|
|
1380
|
+
if m is None:
|
|
1381
|
+
return None
|
|
1382
|
+
indents.add(m.group(1))
|
|
1383
|
+
if len(indents) > 1:
|
|
1384
|
+
return None
|
|
1385
|
+
(indent,) = indents
|
|
1386
|
+
if len({_py_enclosing_line(lines, number, indent, starts) for number in defs}) > 1:
|
|
1387
|
+
return None
|
|
1388
|
+
if not all(_py_overloaded(lines, number, starts) for number in defs[:-1]):
|
|
1389
|
+
return None
|
|
1390
|
+
return defs[0]
|
|
1391
|
+
|
|
1392
|
+
|
|
1272
1393
|
def _js_export_item_binds(item: str, name: str, alias_only: bool = False) -> bool:
|
|
1273
1394
|
"""True when one `export { ... }` item exposes `name`: the name after
|
|
1274
1395
|
`as`, else (unless `alias_only`) the item's own name, an identifier or
|
|
@@ -1469,6 +1590,252 @@ def _js_paren_tail_is_call(after_open: str) -> bool:
|
|
|
1469
1590
|
return False
|
|
1470
1591
|
|
|
1471
1592
|
|
|
1593
|
+
def _js_pattern_bindings(
|
|
1594
|
+
text: str, start: int
|
|
1595
|
+
) -> tuple[list[tuple[str, int]], int | None]:
|
|
1596
|
+
"""The names the destructuring pattern whose `{` or `[` is at
|
|
1597
|
+
`text[start]` binds, each with its offset in `text`, and the offset
|
|
1598
|
+
just past the pattern's closing bracket.
|
|
1599
|
+
|
|
1600
|
+
An object pattern's property is `...target`, a shorthand key or
|
|
1601
|
+
`key: target` (a key is a name, a string, a number or a computed
|
|
1602
|
+
`[...]`, and only a shorthand key binds itself); an array pattern's
|
|
1603
|
+
element is `...target`, a target or a hole; a target is a name or a
|
|
1604
|
+
nested pattern. A `= default` is skipped to the next comma at the
|
|
1605
|
+
pattern's depth, and so is a property or element of any other shape,
|
|
1606
|
+
which binds nothing. '...', "..." and `...` strings are skipped
|
|
1607
|
+
whole, so no bracket or comma inside one counts. Whitespace, newlines
|
|
1608
|
+
included, may sit anywhere between the parts.
|
|
1609
|
+
|
|
1610
|
+
Returns ([], None) when `text[start]` opens no pattern, the pattern
|
|
1611
|
+
does not close in `text`, a string in it does not end, or a bracket in
|
|
1612
|
+
it closes one it did not open. A pure function of its arguments (no
|
|
1613
|
+
module state), so another script can carry a copy of it.
|
|
1614
|
+
"""
|
|
1615
|
+
n = len(text)
|
|
1616
|
+
names: list[tuple[str, int]] = []
|
|
1617
|
+
|
|
1618
|
+
def space(i: int) -> int:
|
|
1619
|
+
while i < n and text[i].isspace():
|
|
1620
|
+
i += 1
|
|
1621
|
+
return i
|
|
1622
|
+
|
|
1623
|
+
def name_end(i: int) -> int:
|
|
1624
|
+
"""The end of the identifier at `i`, `i` itself when none starts there."""
|
|
1625
|
+
if i < n and (text[i].isalpha() or text[i] in "_$"):
|
|
1626
|
+
i += 1
|
|
1627
|
+
while i < n and (text[i].isalnum() or text[i] in "_$"):
|
|
1628
|
+
i += 1
|
|
1629
|
+
return i
|
|
1630
|
+
|
|
1631
|
+
def string_end(i: int) -> int | None:
|
|
1632
|
+
"""The offset past the string whose quote is at `i`."""
|
|
1633
|
+
quote = text[i]
|
|
1634
|
+
i += 1
|
|
1635
|
+
while i < n:
|
|
1636
|
+
if text[i] == "\\":
|
|
1637
|
+
i += 2
|
|
1638
|
+
elif text[i] == quote:
|
|
1639
|
+
return i + 1
|
|
1640
|
+
else:
|
|
1641
|
+
i += 1
|
|
1642
|
+
return None
|
|
1643
|
+
|
|
1644
|
+
def skip(i: int, to_comma: bool = True) -> int | None:
|
|
1645
|
+
"""From `i`, the offset of the next comma (unless not `to_comma`)
|
|
1646
|
+
or closing bracket at depth 0; with `to_comma` False, `text[i]`
|
|
1647
|
+
opens a bracket and the offset past the one closing it."""
|
|
1648
|
+
depth = 0
|
|
1649
|
+
while i < n:
|
|
1650
|
+
ch = text[i]
|
|
1651
|
+
if ch in "'\"`":
|
|
1652
|
+
end = string_end(i)
|
|
1653
|
+
if end is None:
|
|
1654
|
+
return None
|
|
1655
|
+
i = end
|
|
1656
|
+
continue
|
|
1657
|
+
if ch in "([{":
|
|
1658
|
+
depth += 1
|
|
1659
|
+
elif ch in ")]}":
|
|
1660
|
+
if depth == 0:
|
|
1661
|
+
return i
|
|
1662
|
+
depth -= 1
|
|
1663
|
+
if depth == 0 and not to_comma:
|
|
1664
|
+
return i + 1
|
|
1665
|
+
elif ch == "," and depth == 0 and to_comma:
|
|
1666
|
+
return i
|
|
1667
|
+
i += 1
|
|
1668
|
+
return None
|
|
1669
|
+
|
|
1670
|
+
def default(i: int) -> int | None:
|
|
1671
|
+
"""After a target: past its `= default`, when it has one."""
|
|
1672
|
+
i = space(i)
|
|
1673
|
+
if i < n and text[i] == "=" and text[i + 1 : i + 2] not in ("=", ">"):
|
|
1674
|
+
return skip(i + 1)
|
|
1675
|
+
return i
|
|
1676
|
+
|
|
1677
|
+
def bind(i: int, end: int, close: str) -> int | None:
|
|
1678
|
+
"""The name `text[i:end]` is bound when its property or element
|
|
1679
|
+
ends after it or takes a default (any other text: another shape)."""
|
|
1680
|
+
after = space(end)
|
|
1681
|
+
if after < n and (
|
|
1682
|
+
text[after] in (",", close)
|
|
1683
|
+
or text[after] == "=" and text[after + 1 : after + 2] not in ("=", ">")
|
|
1684
|
+
):
|
|
1685
|
+
names.append((text[i:end], i))
|
|
1686
|
+
return default(after)
|
|
1687
|
+
return after
|
|
1688
|
+
|
|
1689
|
+
def target(i: int, close: str) -> int | None:
|
|
1690
|
+
if i < n and text[i] in "{[":
|
|
1691
|
+
end = pattern(i)
|
|
1692
|
+
return None if end is None else default(end)
|
|
1693
|
+
end = name_end(i)
|
|
1694
|
+
return i if end == i else bind(i, end, close)
|
|
1695
|
+
|
|
1696
|
+
def element(i: int, close: str) -> int | None:
|
|
1697
|
+
"""One property or element at `i`: the offset after it."""
|
|
1698
|
+
if text.startswith("...", i):
|
|
1699
|
+
return target(space(i + 3), close)
|
|
1700
|
+
if close == "]":
|
|
1701
|
+
return target(i, close)
|
|
1702
|
+
if text[i] in "'\"":
|
|
1703
|
+
end = string_end(i)
|
|
1704
|
+
elif text[i] == "[":
|
|
1705
|
+
end = skip(i, to_comma=False) # a computed key
|
|
1706
|
+
else:
|
|
1707
|
+
end = name_end(i)
|
|
1708
|
+
if end > i:
|
|
1709
|
+
after = space(end)
|
|
1710
|
+
if after < n and text[after] == ":":
|
|
1711
|
+
return target(space(after + 1), close)
|
|
1712
|
+
return bind(i, end, close) # a shorthand key
|
|
1713
|
+
while end < n and (text[end].isalnum() or text[end] in "._"):
|
|
1714
|
+
end += 1 # a number key
|
|
1715
|
+
if end is None:
|
|
1716
|
+
return None
|
|
1717
|
+
after = space(end)
|
|
1718
|
+
if end > i and after < n and text[after] == ":":
|
|
1719
|
+
return target(space(after + 1), close)
|
|
1720
|
+
return after
|
|
1721
|
+
|
|
1722
|
+
def pattern(i: int) -> int | None:
|
|
1723
|
+
"""The pattern opening at `i`: the offset past its closing bracket."""
|
|
1724
|
+
close = "}" if text[i] == "{" else "]"
|
|
1725
|
+
i += 1
|
|
1726
|
+
while True:
|
|
1727
|
+
i = space(i)
|
|
1728
|
+
if i >= n:
|
|
1729
|
+
return None
|
|
1730
|
+
if text[i] == close:
|
|
1731
|
+
return i + 1
|
|
1732
|
+
if text[i] in ")]}":
|
|
1733
|
+
return None
|
|
1734
|
+
if text[i] == ",":
|
|
1735
|
+
i += 1 # a hole, or a stray comma
|
|
1736
|
+
continue
|
|
1737
|
+
end = element(i, close)
|
|
1738
|
+
if end is None:
|
|
1739
|
+
return None
|
|
1740
|
+
end = space(end)
|
|
1741
|
+
if end < n and text[end] not in (",", close):
|
|
1742
|
+
end = skip(end) # any other shape binds nothing
|
|
1743
|
+
if end is None:
|
|
1744
|
+
return None
|
|
1745
|
+
i = end + 1 if end < n and text[end] == "," else end
|
|
1746
|
+
|
|
1747
|
+
if not (0 <= start < n and text[start] in "{["):
|
|
1748
|
+
return [], None
|
|
1749
|
+
end = pattern(start)
|
|
1750
|
+
return (names, end) if end is not None else ([], None)
|
|
1751
|
+
|
|
1752
|
+
|
|
1753
|
+
def _js_initializer_start(text: str, end: int) -> int | None:
|
|
1754
|
+
"""The offset just past the `=` that begins the initializer of a
|
|
1755
|
+
declarator whose name or pattern ends at `end`, a type annotation
|
|
1756
|
+
(`: T`, brackets, strings and `=>` in it included) skipped. None when
|
|
1757
|
+
neither `=` nor `:` follows, or when a `,` or `;` outside the
|
|
1758
|
+
annotation's brackets, or the end of `text`, comes first."""
|
|
1759
|
+
n = len(text)
|
|
1760
|
+
i = end
|
|
1761
|
+
while i < n and text[i].isspace():
|
|
1762
|
+
i += 1
|
|
1763
|
+
if i < n and text[i] == "=":
|
|
1764
|
+
return None if text[i + 1 : i + 2] == "=" else i + 1
|
|
1765
|
+
if not (i < n and text[i] == ":"):
|
|
1766
|
+
return None
|
|
1767
|
+
i += 1
|
|
1768
|
+
depth = 0
|
|
1769
|
+
while i < n:
|
|
1770
|
+
ch = text[i]
|
|
1771
|
+
if ch in "'\"`":
|
|
1772
|
+
i += 1
|
|
1773
|
+
while i < n and text[i] != ch:
|
|
1774
|
+
i += 2 if text[i] == "\\" else 1
|
|
1775
|
+
elif text.startswith("=>", i):
|
|
1776
|
+
i += 1
|
|
1777
|
+
elif ch in "([{<":
|
|
1778
|
+
depth += 1
|
|
1779
|
+
elif ch in ")]}>":
|
|
1780
|
+
depth -= 1
|
|
1781
|
+
if depth < 0:
|
|
1782
|
+
return None
|
|
1783
|
+
elif ch in ",;" and depth == 0:
|
|
1784
|
+
return None
|
|
1785
|
+
elif ch == "=" and depth == 0:
|
|
1786
|
+
return None if text[i + 1 : i + 2] == "=" else i + 1
|
|
1787
|
+
i += 1
|
|
1788
|
+
return None
|
|
1789
|
+
|
|
1790
|
+
|
|
1791
|
+
def _js_destructured(code_lines: list[str]) -> dict[int, _PatternDecl]:
|
|
1792
|
+
"""For each line that opens a `const` / `let` / `var` statement whose
|
|
1793
|
+
first declarator is a destructuring pattern (`_JS_VAR_DECL_RE`, the
|
|
1794
|
+
rest starting with `{` or `[`): the names the pattern binds, each with
|
|
1795
|
+
the line it appears on, and where its initializer starts.
|
|
1796
|
+
|
|
1797
|
+
The patterns are read from the comment-free lines joined with `\\n`
|
|
1798
|
+
(`_js_pattern_bindings`), so a pattern may span lines and a name's
|
|
1799
|
+
offset maps back to its line. A pattern that does not close, or that
|
|
1800
|
+
nests deeper than the interpreter's recursion limit, binds nothing and
|
|
1801
|
+
gives no item.
|
|
1802
|
+
"""
|
|
1803
|
+
starts: list[tuple[int, int]] = []
|
|
1804
|
+
for number, code in enumerate(code_lines, start=1):
|
|
1805
|
+
if "{" not in code and "[" not in code:
|
|
1806
|
+
continue
|
|
1807
|
+
m = _JS_VAR_DECL_RE.match(code)
|
|
1808
|
+
if m is not None and m.group(1)[:1] in ("{", "["):
|
|
1809
|
+
starts.append((number, m.start(1)))
|
|
1810
|
+
if not starts:
|
|
1811
|
+
return {}
|
|
1812
|
+
text = "\n".join(code_lines)
|
|
1813
|
+
offsets: list[int] = []
|
|
1814
|
+
pos = 0
|
|
1815
|
+
for code in code_lines:
|
|
1816
|
+
offsets.append(pos)
|
|
1817
|
+
pos += len(code) + 1
|
|
1818
|
+
|
|
1819
|
+
def line_of(offset: int) -> int:
|
|
1820
|
+
return bisect.bisect_right(offsets, offset)
|
|
1821
|
+
|
|
1822
|
+
out: dict[int, _PatternDecl] = {}
|
|
1823
|
+
for number, column in starts:
|
|
1824
|
+
try:
|
|
1825
|
+
names, end = _js_pattern_bindings(text, offsets[number - 1] + column)
|
|
1826
|
+
except RecursionError:
|
|
1827
|
+
continue # patterns nested past the interpreter's limit: none bound
|
|
1828
|
+
if end is None:
|
|
1829
|
+
continue
|
|
1830
|
+
value = _js_initializer_start(text, end)
|
|
1831
|
+
at = None
|
|
1832
|
+
if value is not None:
|
|
1833
|
+
line = line_of(value)
|
|
1834
|
+
at = (line, code_lines[line - 1][value - offsets[line - 1] :])
|
|
1835
|
+
out[number] = ([(name, line_of(offset)) for name, offset in names], at)
|
|
1836
|
+
return out
|
|
1837
|
+
|
|
1838
|
+
|
|
1472
1839
|
def _tsjs_definition_lines(
|
|
1473
1840
|
lines: list[str],
|
|
1474
1841
|
code_lines: list[str],
|
|
@@ -1477,6 +1844,7 @@ def _tsjs_definition_lines(
|
|
|
1477
1844
|
declarators: Callable[..., _DeclSplit] = _js_declarators,
|
|
1478
1845
|
declarations_only: bool = False,
|
|
1479
1846
|
quiet: list[bool] | None = None,
|
|
1847
|
+
destructured: dict[int, _PatternDecl] | None = None,
|
|
1480
1848
|
) -> list[tuple[int, bool]]:
|
|
1481
1849
|
"""(line, at column 0) for every line that defines `name`.
|
|
1482
1850
|
|
|
@@ -1486,15 +1854,20 @@ def _tsjs_definition_lines(
|
|
|
1486
1854
|
JSDoc line or a commented-out declaration never matches. A line inside
|
|
1487
1855
|
a multi-line `export { ... }` list or `module.exports = { ... }` object
|
|
1488
1856
|
takes the column of the line the statement starts on, and so do the
|
|
1489
|
-
name line of a declaration whose keyword ends the line above it
|
|
1490
|
-
later declarator on a line of its own
|
|
1857
|
+
name line of a declaration whose keyword ends the line above it, a
|
|
1858
|
+
later declarator on a line of its own and the line of a name that a
|
|
1859
|
+
destructuring pattern binds. `destructured` is
|
|
1860
|
+
`_js_destructured(code_lines)` when the caller has it (it does not
|
|
1861
|
+
depend on the name either): a name its pattern binds counts when the
|
|
1862
|
+
`const` / `let` / `var` line it is keyed by opens a statement here.
|
|
1863
|
+
Class members count only for a
|
|
1491
1864
|
dotted export name. `declarators` splits a declarator list
|
|
1492
1865
|
(`_js_declarators`, or `_SourceCache.js_declarators`, which splits each
|
|
1493
1866
|
line once for every export looked up). With `declarations_only`,
|
|
1494
1867
|
`export import NAME =`, `export default NAME`, an export-list item
|
|
1495
1868
|
without an `as` that renames, a `module.exports` shorthand key and an
|
|
1496
|
-
import-valued binding (a declarator, `exports.NAME =`
|
|
1497
|
-
`module.exports` key whose value starts with `require(` or an
|
|
1869
|
+
import-valued binding (a declarator, a destructured name, `exports.NAME =`
|
|
1870
|
+
or a `module.exports` key whose value starts with `require(` or an
|
|
1498
1871
|
`(await) import(`, on its line or the next one) never count, nor does
|
|
1499
1872
|
a class member.
|
|
1500
1873
|
"""
|
|
@@ -1515,7 +1888,8 @@ def _tsjs_definition_lines(
|
|
|
1515
1888
|
]
|
|
1516
1889
|
# One declarator of a `const` / `let` / `var` list: `NAME = v`,
|
|
1517
1890
|
# `NAME: T = v`, `NAME!: T`, or a bare `NAME`. A destructuring pattern
|
|
1518
|
-
# starts with `{` or `[` and never matches
|
|
1891
|
+
# starts with `{` or `[` and never matches: the names a pattern that
|
|
1892
|
+
# begins the list binds come from `destructured` (`_js_pattern_bindings`).
|
|
1519
1893
|
declarator = re.compile(rf"^\s*{esc}{end}\s*(?:!?\s*:|=|$)")
|
|
1520
1894
|
# With declarations_only, the value bound to NAME (the rest of a
|
|
1521
1895
|
# declarator, of `exports.NAME =` or of a `module.exports` key), to
|
|
@@ -1590,6 +1964,22 @@ def _tsjs_definition_lines(
|
|
|
1590
1964
|
decls: list[tuple[bool, str, str]] = []
|
|
1591
1965
|
if quiet is None:
|
|
1592
1966
|
quiet = _js_quiet_lines(code_lines)
|
|
1967
|
+
if destructured is None:
|
|
1968
|
+
destructured = _js_destructured(code_lines)
|
|
1969
|
+
|
|
1970
|
+
def pattern_binds(number: int) -> list[int]:
|
|
1971
|
+
"""The lines on which the pattern that begins the `const` / `let` /
|
|
1972
|
+
`var` list of line `number` binds `name` (none with
|
|
1973
|
+
`declarations_only` when its value is an import)."""
|
|
1974
|
+
decl = destructured.get(number)
|
|
1975
|
+
if decl is None:
|
|
1976
|
+
return []
|
|
1977
|
+
bound, value = decl
|
|
1978
|
+
at = [line for bound_name, line in bound if bound_name == name]
|
|
1979
|
+
if at and declarations_only and value is not None and imported(value[1], value[0]):
|
|
1980
|
+
return []
|
|
1981
|
+
return at
|
|
1982
|
+
|
|
1593
1983
|
for number, (text, code) in enumerate(zip(lines, code_lines), start=1):
|
|
1594
1984
|
if quiet[number - 1] and not (decls or split or cont) and name not in code:
|
|
1595
1985
|
continue # no rule below can match it: spares a scan per name
|
|
@@ -1637,7 +2027,8 @@ def _tsjs_definition_lines(
|
|
|
1637
2027
|
found.append((number, top))
|
|
1638
2028
|
continue
|
|
1639
2029
|
top = _is_column_0(text)
|
|
1640
|
-
# every rule that matches a line needs the name in it
|
|
2030
|
+
# every rule that matches a line needs the name in it, but for a
|
|
2031
|
+
# destructuring pattern, whose names may sit on the lines below
|
|
1641
2032
|
named = name in code
|
|
1642
2033
|
if named and is_statement(code, number):
|
|
1643
2034
|
found.append((number, top))
|
|
@@ -1647,6 +2038,9 @@ def _tsjs_definition_lines(
|
|
|
1647
2038
|
carry, pieces = declarators(m.group(1))
|
|
1648
2039
|
if carry is not None:
|
|
1649
2040
|
decls.append((top, *carry))
|
|
2041
|
+
# `export const { a, b: NAME } = obj;`, on one line or several:
|
|
2042
|
+
# each name's line, at the statement's column
|
|
2043
|
+
found.extend((line, top) for line in pattern_binds(number))
|
|
1650
2044
|
if named and any(declares(p, number) for p in pieces):
|
|
1651
2045
|
# a later declarator: `export const a = 1, NAME = 2;`
|
|
1652
2046
|
found.append((number, top))
|
|
@@ -1766,7 +2160,7 @@ def _line_matches(
|
|
|
1766
2160
|
code_lines = cache.js_code_lines(path, lines)
|
|
1767
2161
|
found = _tsjs_definition_lines(
|
|
1768
2162
|
lines, code_lines, name, dotted, cache.js_declarators, declarations_only,
|
|
1769
|
-
cache.js_quiet_lines(path, code_lines),
|
|
2163
|
+
cache.js_quiet_lines(path, code_lines), cache.js_destructured(path, code_lines),
|
|
1770
2164
|
)
|
|
1771
2165
|
if declarations_only:
|
|
1772
2166
|
top = {n for n, at_column_0 in found if at_column_0}
|
|
@@ -1790,6 +2184,39 @@ def find_definition_lines(
|
|
|
1790
2184
|
return None if matches is None else matches[0]
|
|
1791
2185
|
|
|
1792
2186
|
|
|
2187
|
+
def _missed_definition_lines(
|
|
2188
|
+
source_file: str,
|
|
2189
|
+
export_name: str,
|
|
2190
|
+
source_root: Path,
|
|
2191
|
+
cache: _SourceCache,
|
|
2192
|
+
line: int,
|
|
2193
|
+
defs: list[int],
|
|
2194
|
+
every: set[int],
|
|
2195
|
+
) -> list[int]:
|
|
2196
|
+
"""The `definition_lines` a finding lists for a recorded `line` that is
|
|
2197
|
+
not one of `defs` (`_definition_matches` over the same `cache`).
|
|
2198
|
+
|
|
2199
|
+
A recorded line that is an indented match the column-0 rule dropped
|
|
2200
|
+
joins them, so the finding carries several lines and nobody moves it.
|
|
2201
|
+
Otherwise a Python file's `@overload` group gives its first line alone
|
|
2202
|
+
(`_python_overload_start`), the line to move the entry to, and any
|
|
2203
|
+
other answer gives `defs` as they are.
|
|
2204
|
+
"""
|
|
2205
|
+
if line in every:
|
|
2206
|
+
return sorted(set(defs) | {line})
|
|
2207
|
+
rel = source_file.strip().replace("\\", "/")
|
|
2208
|
+
if Path(rel).suffix.lower() not in PYTHON_EXTENSIONS or len(defs) < 2:
|
|
2209
|
+
return defs
|
|
2210
|
+
path = (source_root / rel).resolve()
|
|
2211
|
+
lines = cache.lines(path)
|
|
2212
|
+
if lines is None:
|
|
2213
|
+
return defs
|
|
2214
|
+
first = _python_overload_start(
|
|
2215
|
+
lines, definition_name(export_name), defs, cache.py_statement_starts(path, lines)
|
|
2216
|
+
)
|
|
2217
|
+
return defs if first is None else [first]
|
|
2218
|
+
|
|
2219
|
+
|
|
1793
2220
|
def definition_lines_report(
|
|
1794
2221
|
source_file: str,
|
|
1795
2222
|
export_name: str,
|
|
@@ -1802,9 +2229,11 @@ def definition_lines_report(
|
|
|
1802
2229
|
|
|
1803
2230
|
`line_check` is `file-missing`, `skipped-export-type` (a `module` or
|
|
1804
2231
|
`package`, no definition line), `skipped-language` (no rule for the
|
|
1805
|
-
extension) or `checked`. With `line`, a recorded line that is
|
|
1806
|
-
|
|
1807
|
-
`
|
|
2232
|
+
extension) or `checked`. With `line`, a recorded line that is not a
|
|
2233
|
+
definition line gets the lines `verify` would list
|
|
2234
|
+
(`_missed_definition_lines`): an indented match the column-0 rule
|
|
2235
|
+
dropped joins `definition_lines`, so the answer carries several lines,
|
|
2236
|
+
and a Python `@overload` group gives its first line alone.
|
|
1808
2237
|
"""
|
|
1809
2238
|
report: dict = {
|
|
1810
2239
|
"source_file": source_file,
|
|
@@ -1823,14 +2252,17 @@ def definition_lines_report(
|
|
|
1823
2252
|
covered = suffix in PYTHON_EXTENSIONS or suffix in TSJS_EXTENSIONS
|
|
1824
2253
|
report["line_check"] = "skipped-export-type" if covered else "skipped-language"
|
|
1825
2254
|
return report
|
|
1826
|
-
|
|
2255
|
+
cache = _SourceCache()
|
|
2256
|
+
matches = _definition_matches(source_file, export_name, source_root, cache)
|
|
1827
2257
|
if matches is None:
|
|
1828
2258
|
raise ValueError(f"failed to read source file {source_root / rel}")
|
|
1829
2259
|
defs, every = matches
|
|
1830
2260
|
if line is not None:
|
|
1831
2261
|
report["line_is_definition"] = line in defs
|
|
1832
|
-
if line not in defs
|
|
1833
|
-
defs =
|
|
2262
|
+
if line not in defs:
|
|
2263
|
+
defs = _missed_definition_lines(
|
|
2264
|
+
source_file, export_name, source_root, cache, line, defs, every
|
|
2265
|
+
)
|
|
1834
2266
|
report["definition_lines"] = defs
|
|
1835
2267
|
return report
|
|
1836
2268
|
|
|
@@ -2161,8 +2593,8 @@ def baseline_gaps(
|
|
|
2161
2593
|
) -> tuple[list[dict], list[dict]]:
|
|
2162
2594
|
"""The provenance entries an extraction left out whose cited file still
|
|
2163
2595
|
declares them: the recipe runner never reports a module, an alias that
|
|
2164
|
-
renames, a dunder such as `__version__` or another
|
|
2165
|
-
a diff would read each as removed.
|
|
2596
|
+
renames, a dunder such as `__version__` or another Python or Rust
|
|
2597
|
+
underscore name, so a diff would read each as removed.
|
|
2166
2598
|
|
|
2167
2599
|
`held` holds the (name, file) of every export the extraction found,
|
|
2168
2600
|
`files` the files whose entries are checked (the ones read), and
|
|
@@ -3291,13 +3723,13 @@ def verify(
|
|
|
3291
3723
|
continue
|
|
3292
3724
|
defs, every = matches
|
|
3293
3725
|
if line not in defs:
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
# lists several lines and nobody auto-moves it.
|
|
3298
|
-
defs = sorted(set(defs) | {line})
|
|
3726
|
+
# An indented match the column-0 rule dropped joins
|
|
3727
|
+
# the lines, so nobody auto-moves it; an @overload
|
|
3728
|
+
# group gives its first line alone, the move target.
|
|
3299
3729
|
item["reason"] = STALE_LINE_NOT_DEFINITION
|
|
3300
|
-
item["definition_lines"] =
|
|
3730
|
+
item["definition_lines"] = _missed_definition_lines(
|
|
3731
|
+
sf, name, source_root, cache, line, defs, every
|
|
3732
|
+
)
|
|
3301
3733
|
stale.append(item)
|
|
3302
3734
|
stale.sort(key=lambda s: (s["export_name"], s.get("source_file") or ""))
|
|
3303
3735
|
|