bmad-module-skill-forge 8.0.0 → 8.1.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/docs/_data/pinned.yaml +7 -7
- package/docs/verifying-a-skill.md +7 -7
- package/docs/workflows.md +1 -1
- package/package.json +1 -1
- package/src/shared/scripts/skf-verify-provenance-completeness.py +331 -105
- package/src/skf-analyze-source/SKILL.md +1 -1
- package/src/skf-audit-skill/SKILL.md +1 -1
- package/src/skf-brief-skill/SKILL.md +1 -1
- package/src/skf-campaign/SKILL.md +1 -1
- package/src/skf-create-skill/SKILL.md +1 -1
- package/src/skf-create-stack-skill/SKILL.md +1 -1
- package/src/skf-drop-skill/SKILL.md +1 -1
- package/src/skf-export-skill/SKILL.md +1 -1
- package/src/skf-forger/SKILL.md +1 -1
- package/src/skf-quick-skill/SKILL.md +1 -1
- package/src/skf-refine-architecture/SKILL.md +1 -1
- package/src/skf-rename-skill/SKILL.md +1 -1
- package/src/skf-setup/SKILL.md +1 -1
- package/src/skf-test-skill/SKILL.md +1 -1
- package/src/skf-test-skill/customize.toml +10 -0
- package/src/skf-test-skill/references/init.md +1 -1
- package/src/skf-test-skill/references/report.md +2 -1
- package/src/skf-test-skill/references/scoring-rules.md +2 -2
- package/src/skf-test-skill/scripts/gap-ledger.py +61 -39
- package/src/skf-test-skill/templates/test-report-template.md +4 -3
- package/src/skf-update-skill/SKILL.md +1 -1
- package/src/skf-verify-stack/SKILL.md +1 -1
|
@@ -476,17 +476,21 @@ Subcommands:
|
|
|
476
476
|
`fabricated` is true, with reason `not-defined`, when the name has an
|
|
477
477
|
entry and every entry's `line_check` is `file-missing`, or `checked`
|
|
478
478
|
with an empty `definition_lines`: the export is in neither the surface
|
|
479
|
-
nor the cited file, and no line elsewhere renames to it
|
|
480
|
-
declaration a cited file holds. Such a
|
|
481
|
-
`aliased` and `fabricated` false, and its line
|
|
482
|
-
the first, per file of the name's declarations the
|
|
483
|
-
(its `declared_in`, else the cited file's) and per
|
|
484
|
-
that file in order, that renames to NAME a name `L`
|
|
485
|
-
`checked` entry of the name declares by the same
|
|
486
|
-
package so named counting; for `L` `default`, a
|
|
487
|
-
`export default`, `export { X as default }` or
|
|
488
|
-
|
|
489
|
-
|
|
479
|
+
nor the cited file, and no line elsewhere renames to it, directly or
|
|
480
|
+
through a chain of renames, the declaration a cited file holds. Such a
|
|
481
|
+
rename gives the reason `aliased` and `fabricated` false, and its line
|
|
482
|
+
is the `defined_at`: the first, per file of the name's declarations the
|
|
483
|
+
lookup below finds (its `declared_in`, else the cited file's) and per
|
|
484
|
+
candidate line of that file in order, that renames to NAME a name `L`
|
|
485
|
+
which a file of a `checked` entry of the name declares by the same
|
|
486
|
+
rule (no module or package so named counting; for `L` `default`, a
|
|
487
|
+
column-0 `export default`, `export { X as default }` or
|
|
488
|
+
`exports.default =`), or which a file the rename resolves to declares
|
|
489
|
+
or renames, by the chain below, the chain ending at such a cited
|
|
490
|
+
declaration. The name that cited file declares where the chain ends
|
|
491
|
+
(for a one-step rename, the `L` the line at `defined_at` renames) is
|
|
492
|
+
the item's `renames`, a key no other item has, so that test-skill's
|
|
493
|
+
provenance line gap can say how to record the rename.
|
|
490
494
|
The renames read:
|
|
491
495
|
- an export-list item `L as NAME` (`export { ... }`,
|
|
492
496
|
`export type { ... }`, on one line or an item of a multi-line
|
|
@@ -499,14 +503,25 @@ Subcommands:
|
|
|
499
503
|
`as`, when a column-0 import binds NAME to another name
|
|
500
504
|
(`import { L as NAME }`, or `import NAME from`, `L` `default`), at
|
|
501
505
|
the export;
|
|
502
|
-
- a column-0 `const`, `let` or `var NAME = L
|
|
503
|
-
|
|
504
|
-
|
|
506
|
+
- a column-0 `const`, `let` or `var NAME = L`, annotated or not
|
|
507
|
+
(`const NAME: <type> = L`, the type on the line, holding no `;`
|
|
508
|
+
and no `=` but in `=>`), `type NAME = L` or a generic
|
|
509
|
+
`type NAME<P1, ...> = L<P1, ...>` whose arguments are its bare
|
|
510
|
+
parameters, the same names in the same order, exported or not,
|
|
511
|
+
`L` a bare identifier and the statement ending on its line;
|
|
512
|
+
- for NAME `default`, a column-0 `export default L` (the `;`
|
|
513
|
+
optional), `L` a bare identifier that a column-0 import or
|
|
514
|
+
declaration of the same file binds and the statement ending on its
|
|
515
|
+
line (`import { createEntry } from './api'` then
|
|
516
|
+
`export default createEntry;`);
|
|
505
517
|
- a Python `from X import L as NAME` item (on one line, in
|
|
506
|
-
parentheses or after a backslash), the statement at column 0
|
|
507
|
-
|
|
508
|
-
block
|
|
509
|
-
`
|
|
518
|
+
parentheses or after a backslash), the statement at column 0, one
|
|
519
|
+
level in a column-0 `try`, `except`, `else`, `if`, `elif`, `with`
|
|
520
|
+
or `finally` block or as such a header's one-line body after its
|
|
521
|
+
first colon (`try: from X import L as NAME`), as compatibility
|
|
522
|
+
imports are written, and a column-0 Python `NAME = L` or
|
|
523
|
+
`NAME: <type> = L` (the type holding no `;` or `=`), `L` a bare
|
|
524
|
+
identifier.
|
|
510
525
|
A TS/JS line is read with its comments removed (an export list split
|
|
511
526
|
on a copy whose one-line strings are blanked), a Python line with its
|
|
512
527
|
`#` comment cut. `L` comes from the rename's `from` module, else from
|
|
@@ -519,26 +534,53 @@ Subcommands:
|
|
|
519
534
|
(`.d.ts` included); a Python relative module from the file's folder
|
|
520
535
|
(`.` the folder's own `__init__`), or an absolute one below the parent
|
|
521
536
|
of the file's package root, as `m.py`, `m.pyi`, `m/__init__.py` or
|
|
522
|
-
`m/__init__.pyi`. An `L` with no module
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
of
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
a
|
|
541
|
-
`
|
|
537
|
+
`m/__init__.pyi`. An `L` with no module resolves to the renaming file
|
|
538
|
+
itself. When one of the files the rename resolves to that declare `L`
|
|
539
|
+
is a cited file, the rename is an alias. Otherwise the rename is
|
|
540
|
+
followed as a chain through each of those files that declares `L` or
|
|
541
|
+
renames it (a middle file): each candidate line of `L` there that
|
|
542
|
+
renames it, by a form above (an export-list alias, a compatibility
|
|
543
|
+
import in a block or on one line, an import then a plain re-export,
|
|
544
|
+
an assignment of a bare `L`, annotated or not, an `export default L`
|
|
545
|
+
of a bound `L`, which a rename of `default` reaches:
|
|
546
|
+
`export { default as NAME } from`, or `import NAME from` re-exported)
|
|
547
|
+
or by a Python column-0 `L = M.L2`, annotated or not, whose `M` a
|
|
548
|
+
column-0 `from X import M` binds (the module `X` joined with `M`, `.`
|
|
549
|
+
prefixes kept), is the next step, read and resolved the same way, until
|
|
550
|
+
a step reaches a cited file that declares its name (an alias) or none
|
|
551
|
+
does (fabricated, so a rename of another file's same-named `L` stays
|
|
552
|
+
fabricated). Each (file, name) pair, a resolved file that declares
|
|
553
|
+
or renames the name, is followed once per first rename, so a chain that
|
|
554
|
+
loops ends, fabricated. When the first rename's module resolves to no
|
|
555
|
+
file (a package name, never looked up in `node_modules`, a tsconfig
|
|
556
|
+
`paths` alias, a package `exports` map, a path with no such file) or
|
|
557
|
+
to files that declare no `L` (a barrel that re-exports it), any cited
|
|
558
|
+
file that declares `L` clears the name; at a later step of a chain,
|
|
559
|
+
such a module ends the chain, fabricated, unless one of those files
|
|
560
|
+
renames `L` (a middle file, followed as above). The suspects' cited pass
|
|
561
|
+
settles no such name at line 1 of a cited module or package so named
|
|
562
|
+
before the walk, even when the module is itself the export (a column-0
|
|
563
|
+
`export default` or `module.exports =`), so the walk always runs; the
|
|
564
|
+
walk keeps its module rule, so `declared_in` may list that module at
|
|
565
|
+
its line 1 when the walk reads its folder. Any other rename stays
|
|
566
|
+
fabricated: a TS type assertion (`x as NAME`), a
|
|
567
|
+
call or any other expression (`const NAME = make()`, and `NAME = a.b`
|
|
568
|
+
at the documented name's own line: `L = M.L2` is only a later step of
|
|
569
|
+
a chain), a default export of any other expression
|
|
570
|
+
(`export default make();`, an object, an arrow, a value that goes on
|
|
571
|
+
to the next line, an indented `export default L`, an `L` no import or
|
|
572
|
+
declaration of the file binds), a generic alias whose parameters are
|
|
573
|
+
constrained, defaulted, swapped or instantiated
|
|
574
|
+
(`type NAME<T extends X> = L<T>`, `type NAME<T = X> = L<T>`,
|
|
575
|
+
`type NAME<K, V> = L<V, K>`, `type NAME = L<string>`),
|
|
576
|
+
`export * as NAME from`, Python
|
|
577
|
+
`import M as NAME`, a rename of a module (`import * as L`,
|
|
578
|
+
`from . import L as NAME`: no module or package so named declares
|
|
579
|
+
`L`), a rename inside a `def`, a `class` or a deeper block (two levels
|
|
580
|
+
in a `with` block included), `as NAME` in a comment or a string, a
|
|
581
|
+
`default as NAME` whose module has only `module.exports =`, and a
|
|
582
|
+
chain that leaves the files the resolver reads, loops or ends at
|
|
583
|
+
another declaration.
|
|
542
584
|
Otherwise the reason is `defined` (a cited file defines the name),
|
|
543
585
|
`unchecked` (no rule could check a cited file, and none defines it) or
|
|
544
586
|
`no-entry` (the map has no entry for the name).
|
|
@@ -2870,18 +2912,37 @@ _JS_IMPORT_BARE_RE = re.compile(r"""^\s*import\s*["']""")
|
|
|
2870
2912
|
_JS_IMPORT_ITEM_RE = re.compile(rf"^(?:type\s+)?({_JS_NAME_OR_STRING})(?:\s+as\s+([\w$]+))?$")
|
|
2871
2913
|
_JS_IMPORT_DEFAULT_RE = re.compile(r"^([\w$]+)\s*(?:,|$)")
|
|
2872
2914
|
_JS_IMPORT_NAMESPACE_RE = re.compile(r"^\*\s*as\s+([\w$]+)")
|
|
2873
|
-
# `const|let|var NAME = L`
|
|
2874
|
-
#
|
|
2915
|
+
# `const|let|var NAME = L` (or `NAME: <type> = L`, the type on the line,
|
|
2916
|
+
# holding no `;`, no `=` but in `=>` and no `,` that opens another
|
|
2917
|
+
# declarator) and `type NAME = L` (or `type NAME<P1, ...> = L<A1, ...>`,
|
|
2918
|
+
# one trailing comma allowed in either list), exported or not:
|
|
2919
|
+
# NAME (`var` or `type`), the type's parameters, L, its arguments, and the
|
|
2920
|
+
# `;` when the line ends the statement (`end`). `renames` reads a generic
|
|
2921
|
+
# one only when its arguments are its bare parameters in order
|
|
2922
|
+
# (`_js_passes_parameters`).
|
|
2875
2923
|
_JS_NAME_IS_RE = re.compile(
|
|
2876
|
-
r"^\s*(?:export\s+)?(?:const|let|var
|
|
2924
|
+
r"^\s*(?:export\s+)?(?:(?:const|let|var)\s+(?P<var>[\w$]+)"
|
|
2925
|
+
r"(?:\s*:(?!=)\s*(?:[^=;,\s]|=>|,(?!\s*[\w$]+\s*=(?![=>])))(?:[^=;,]|=>|,(?!\s*[\w$]+\s*=(?![=>])))*?)?"
|
|
2926
|
+
r"|type\s+(?P<type>[\w$]+)(?:\s*<(?P<params>[^<>]*)>)?)"
|
|
2927
|
+
r"\s*(?<![=!])=(?![=>])\s*(?P<value>[A-Za-z_$][\w$]*)(?:\s*<(?P<args>[^<>]*)>)?\s*(?P<end>;?)$"
|
|
2877
2928
|
)
|
|
2929
|
+
# `export default L`, `L` a bare identifier, and the `;` when the line ends
|
|
2930
|
+
# the statement (`end`): for the name `default`, `renames` reads it as a
|
|
2931
|
+
# rename of `L` when an import or a declaration of the same file binds `L`.
|
|
2932
|
+
_JS_EXPORT_DEFAULT_IS_RE = re.compile(r"^\s*export\s+default\s+(?P<value>[A-Za-z_$][\w$]*)\s*(?P<end>;?)\s*$")
|
|
2878
2933
|
# A line that begins with one of these goes on with the value above it (a
|
|
2879
2934
|
# member, an operator, a call, an index or a tagged template).
|
|
2880
2935
|
_JS_VALUE_GOES_ON = _JS_CONTINUATION_START | frozenset("([`")
|
|
2881
2936
|
# Python's `from X import ...`, with its module.
|
|
2882
2937
|
_PY_FROM_CLAUSE_RE = re.compile(r"^\s*from\s+([\w.]+)\s+import\s+(.*)$")
|
|
2883
2938
|
# The column-0 header of a block whose body may hold a compatibility import.
|
|
2884
|
-
_PY_BLOCK_HEADER_RE = re.compile(r"^(?:try|except|else|if|elif)(?!\w)")
|
|
2939
|
+
_PY_BLOCK_HEADER_RE = re.compile(r"^(?:try|except|else|if|elif|with|finally)(?!\w)")
|
|
2940
|
+
# A column-0 Python `NAME = L` or `NAME: <type> = L`, the type holding no
|
|
2941
|
+
# `=` or `;` (`{name}` escaped): L.
|
|
2942
|
+
_PY_NAME_IS = r"^{name}\s*(?::(?!=)[^=;]+?)?(?<![=!<>])=(?!=)\s*([A-Za-z_]\w*)\s*$"
|
|
2943
|
+
# A column-0 Python `NAME = M.L2` or `NAME: <type> = M.L2` (`{name}`
|
|
2944
|
+
# escaped): (M, L2), a step of a rename chain only (`_Renames.member`).
|
|
2945
|
+
_PY_NAME_IS_MEMBER = r"^{name}\s*(?::(?!=)[^=;]+?)?(?<![!<>])=(?!=)\s*([A-Za-z_]\w*)\.([A-Za-z_]\w*)\s*$"
|
|
2885
2946
|
# The files a relative TS/JS module names, after the path itself:
|
|
2886
2947
|
# `<path><ext>`, then `<path>/index<ext>`.
|
|
2887
2948
|
_TSJS_MODULE_EXTENSIONS = (".ts", ".tsx", ".d.ts", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs")
|
|
@@ -2913,6 +2974,20 @@ def _js_export_sides(item: str) -> tuple[str, str | None] | None:
|
|
|
2913
2974
|
return own, exposed
|
|
2914
2975
|
|
|
2915
2976
|
|
|
2977
|
+
def _js_passes_parameters(params: str | None, args: str | None) -> bool:
|
|
2978
|
+
"""True when a `NAME = L` line's type parameters (`type NAME<P1, ...>`)
|
|
2979
|
+
reach `L` unchanged: neither parameters nor arguments, or bare
|
|
2980
|
+
parameters given as `L`'s arguments, the same names in the same order
|
|
2981
|
+
(`type NAME<K, V> = L<K, V>`). False for a constraint or a default
|
|
2982
|
+
(`<T extends X>`, `<T = X>`), a swap (`L<V, K>`), an instantiation
|
|
2983
|
+
(`L<string>`) and arguments with no parameters."""
|
|
2984
|
+
if params is None or args is None:
|
|
2985
|
+
return params is None and args is None
|
|
2986
|
+
names = [p.strip() for p in params.strip().removesuffix(",").split(",")]
|
|
2987
|
+
given = [a.strip() for a in args.strip().removesuffix(",").split(",")]
|
|
2988
|
+
return all(_JS_IDENT_RE.fullmatch(n) for n in names) and names == given
|
|
2989
|
+
|
|
2990
|
+
|
|
2916
2991
|
def _js_export_lists(
|
|
2917
2992
|
lines: list[str], code: list[str]
|
|
2918
2993
|
) -> list[tuple[list[tuple[int, str]], str | None]]:
|
|
@@ -3006,18 +3081,32 @@ def _js_import_bindings(lines: list[str], code: list[str]) -> list[_Binding]:
|
|
|
3006
3081
|
return found
|
|
3007
3082
|
|
|
3008
3083
|
|
|
3084
|
+
def _py_from_clause(code: str) -> re.Match[str] | None:
|
|
3085
|
+
"""The `from X import` clause (`_PY_FROM_CLAUSE_RE`) a Python statement
|
|
3086
|
+
line opens, its `#` comment cut: the line itself, or the one-line body
|
|
3087
|
+
after the first colon of a column-0 `try`, `except`, `else`, `if`,
|
|
3088
|
+
`elif`, `with` or `finally` header (`try: from X import L as NAME`)."""
|
|
3089
|
+
found = _PY_FROM_CLAUSE_RE.match(code)
|
|
3090
|
+
if found is None and _PY_BLOCK_HEADER_RE.match(code) and ":" in code:
|
|
3091
|
+
found = _PY_FROM_CLAUSE_RE.match(code.split(":", 1)[1])
|
|
3092
|
+
return found
|
|
3093
|
+
|
|
3094
|
+
|
|
3009
3095
|
def _py_block_imports(lines: list[str], code: list[str], starts: set[int]) -> set[int]:
|
|
3010
3096
|
"""The lines (from 1) on which a `from X import` statement starts one
|
|
3011
|
-
level under a column-0 `try`, `except`, `else`, `if
|
|
3012
|
-
compatibility import),
|
|
3013
|
-
|
|
3014
|
-
|
|
3097
|
+
level under a column-0 `try`, `except`, `else`, `if`, `elif`, `with` or
|
|
3098
|
+
`finally` (a compatibility import), or as the one-line body of such a
|
|
3099
|
+
header, after its first colon (`try: from X import L as NAME`), never
|
|
3100
|
+
in a `def`, a `class` or deeper. `code` holds the lines with their `#`
|
|
3101
|
+
comments cut, `starts` the statement starts (`_py_statement_starts`)."""
|
|
3015
3102
|
found: set[int] = set()
|
|
3016
3103
|
header, level = False, None
|
|
3017
3104
|
for number in sorted(starts):
|
|
3018
3105
|
text = lines[number - 1]
|
|
3019
3106
|
if _is_column_0(text):
|
|
3020
3107
|
header, level = _PY_BLOCK_HEADER_RE.match(code[number - 1]) is not None, None
|
|
3108
|
+
if header and _py_from_clause(code[number - 1]) is not None:
|
|
3109
|
+
found.add(number) # the header's own one-line body
|
|
3021
3110
|
continue
|
|
3022
3111
|
indent = len(text) - len(text.lstrip())
|
|
3023
3112
|
if level is None:
|
|
@@ -3032,11 +3121,12 @@ def _py_from_clauses(
|
|
|
3032
3121
|
) -> list[tuple[int, str, list[tuple[int, str]]]]:
|
|
3033
3122
|
"""(line from 1, module, items) of each `from X import` statement that
|
|
3034
3123
|
starts on a line `keep` accepts, an item as (line from 1, its text):
|
|
3035
|
-
on one line, in parentheses or continued with a backslash
|
|
3124
|
+
on one line, in parentheses or continued with a backslash, a block
|
|
3125
|
+
header's one-line body included (`_py_from_clause`)."""
|
|
3036
3126
|
clauses: list[tuple[int, str, list[tuple[int, str]]]] = []
|
|
3037
3127
|
i, n = 0, len(code)
|
|
3038
3128
|
while i < n:
|
|
3039
|
-
opened =
|
|
3129
|
+
opened = _py_from_clause(code[i]) if i + 1 in starts and keep(i + 1) else None
|
|
3040
3130
|
if opened is None:
|
|
3041
3131
|
i += 1
|
|
3042
3132
|
continue
|
|
@@ -3075,6 +3165,7 @@ class _Renames:
|
|
|
3075
3165
|
def __init__(self, parts: tuple[str, ...], lines: list[str], cache: _SourceCache) -> None:
|
|
3076
3166
|
self.parts = parts
|
|
3077
3167
|
self.lines = lines
|
|
3168
|
+
self.cache = cache
|
|
3078
3169
|
self.text = "\n".join(lines)
|
|
3079
3170
|
path = Path(*parts)
|
|
3080
3171
|
self.python = _rule_family(parts[-1]) == "python"
|
|
@@ -3091,8 +3182,9 @@ class _Renames:
|
|
|
3091
3182
|
# -- reading, once per file ---------------------------------------------
|
|
3092
3183
|
|
|
3093
3184
|
def _py_clauses(self) -> list[tuple[int, str, list[tuple[int, str]]]]:
|
|
3094
|
-
"""The `from X import` statements at column 0
|
|
3095
|
-
column-0 block
|
|
3185
|
+
"""The `from X import` statements at column 0, one level in a
|
|
3186
|
+
column-0 block or a block header's one-line body
|
|
3187
|
+
(`_py_block_imports`)."""
|
|
3096
3188
|
if self._clauses is None:
|
|
3097
3189
|
if self.starts is None:
|
|
3098
3190
|
self._clauses = []
|
|
@@ -3110,7 +3202,9 @@ class _Renames:
|
|
|
3110
3202
|
return self._lists
|
|
3111
3203
|
|
|
3112
3204
|
def _imports(self) -> list[_Binding]:
|
|
3113
|
-
"""The names the file's column-0 imports bind (`_Binding`)
|
|
3205
|
+
"""The names the file's column-0 imports bind (`_Binding`); a
|
|
3206
|
+
compatibility import, one level in a block or a block header's
|
|
3207
|
+
one-line body, binds none."""
|
|
3114
3208
|
if self._bindings is None:
|
|
3115
3209
|
if not self.python:
|
|
3116
3210
|
self._bindings = _js_import_bindings(self.lines, self.code)
|
|
@@ -3119,7 +3213,9 @@ class _Renames:
|
|
|
3119
3213
|
else:
|
|
3120
3214
|
found: list[_Binding] = []
|
|
3121
3215
|
for start, module, items in self._py_clauses():
|
|
3122
|
-
if not _is_column_0(self.lines[start - 1])
|
|
3216
|
+
if not _is_column_0(self.lines[start - 1]) or (
|
|
3217
|
+
_PY_FROM_CLAUSE_RE.match(self.code[start - 1]) is None # `try: from ...`
|
|
3218
|
+
):
|
|
3123
3219
|
continue
|
|
3124
3220
|
for _, item in items:
|
|
3125
3221
|
m = _PY_IMPORT_ITEM_RE.match(item)
|
|
@@ -3175,10 +3271,11 @@ class _Renames:
|
|
|
3175
3271
|
for `name`: every column-0 line that declares it (`declared_line`'s
|
|
3176
3272
|
rule; with `module_fallback`, line 1 of the module or package so
|
|
3177
3273
|
named when none does), a Python `from X import L as NAME` item one
|
|
3178
|
-
level in a column-0 `try`, `except`, `else`, `if
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
`import NAME
|
|
3274
|
+
level in a column-0 `try`, `except`, `else`, `if`, `elif`, `with`
|
|
3275
|
+
or `finally` block or as such a header's one-line body
|
|
3276
|
+
(`try: from X import L as NAME`), and a TS/JS local export-list
|
|
3277
|
+
item `NAME` without `as` when a column-0 import binds NAME to
|
|
3278
|
+
another name (`import { L as NAME }`, `import NAME from`)."""
|
|
3182
3279
|
if not name or "." in name:
|
|
3183
3280
|
return []
|
|
3184
3281
|
path = Path(*self.parts)
|
|
@@ -3223,20 +3320,38 @@ class _Renames:
|
|
|
3223
3320
|
- a local export-list item `NAME` without `as`, when a column-0
|
|
3224
3321
|
import binds NAME to another name (`import { L as NAME }`, or
|
|
3225
3322
|
`import NAME from`, `L` `default`);
|
|
3226
|
-
- a column-0 `const`, `let` or `var NAME = L
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3323
|
+
- a column-0 `const`, `let` or `var NAME = L`, annotated or not
|
|
3324
|
+
(`const NAME: <type> = L`, the type on the line, holding no `;`
|
|
3325
|
+
and no `=` but in `=>`), `type NAME = L` or a generic
|
|
3326
|
+
`type NAME<P1, ...> = L<P1, ...>` whose arguments are its bare
|
|
3327
|
+
parameters, the same names in the same order, exported or not,
|
|
3328
|
+
`L` a bare identifier and the statement ending there;
|
|
3329
|
+
- for `name` `default`, a column-0 `export default L`
|
|
3330
|
+
(`_JS_EXPORT_DEFAULT_IS_RE`, the `;` optional), `L` a bare
|
|
3331
|
+
identifier and the statement ending there: `L` taken from the
|
|
3332
|
+
import that binds it (`binding`) or, when the file declares `L`
|
|
3333
|
+
(`declared_line`, no module so named), from the file itself;
|
|
3334
|
+
- a Python `from X import L as NAME` item, the statement at column 0,
|
|
3335
|
+
one level in a column-0 `try`, `except`, `else`, `if`, `elif`,
|
|
3336
|
+
`with` or `finally` block or as such a header's one-line body
|
|
3337
|
+
(`try: from X import L as NAME`), and a column-0 `NAME = L` or
|
|
3338
|
+
`NAME: <type> = L` (the type holding no `;` or `=`), `L` a bare
|
|
3339
|
+
identifier.
|
|
3232
3340
|
|
|
3233
3341
|
The module is the list's or the clause's `from`; a rename with none
|
|
3234
3342
|
takes the column-0 import that binds `L` (`binding`), `L` becoming
|
|
3235
3343
|
its imported name, and a namespace import's name renames nothing.
|
|
3236
3344
|
Empty for any other line: `as NAME` in a comment or a string, a TS
|
|
3237
3345
|
type assertion (`const NAME = x as NAME`), a call or any other
|
|
3238
|
-
expression
|
|
3239
|
-
|
|
3346
|
+
expression (`NAME = a.b` included: `member` reads it, as a step of
|
|
3347
|
+
a chain only), a default export of anything but a bare `L` an
|
|
3348
|
+
import or a declaration of the file binds (`export default make()`,
|
|
3349
|
+
an object, an arrow, a value that goes on to the next line, a
|
|
3350
|
+
namespace import's `L`), a generic alias whose parameters are
|
|
3351
|
+
constrained, defaulted, swapped or instantiated (`type NAME<T
|
|
3352
|
+
extends X> = L<T>`, `type NAME<K, V> = L<V, K>`, `type NAME =
|
|
3353
|
+
L<string>`), a Python `import M as NAME` and a rename inside a
|
|
3354
|
+
`def` or a `class`."""
|
|
3240
3355
|
if not 0 < number <= len(self.lines):
|
|
3241
3356
|
return []
|
|
3242
3357
|
found: list[tuple[str | None, str | None]] = []
|
|
@@ -3249,7 +3364,7 @@ class _Renames:
|
|
|
3249
3364
|
found.append((local, module))
|
|
3250
3365
|
starts = self.starts or set()
|
|
3251
3366
|
if number in starts and _is_column_0(self.lines[index]):
|
|
3252
|
-
m = re.match(
|
|
3367
|
+
m = re.match(_PY_NAME_IS.format(name=re.escape(name)), self.code[index])
|
|
3253
3368
|
if m is not None and m.group(1) != name:
|
|
3254
3369
|
found.append(self._bound(m.group(1), number))
|
|
3255
3370
|
return [(local, module) for local, module in found if local is not None]
|
|
@@ -3269,12 +3384,45 @@ class _Renames:
|
|
|
3269
3384
|
elif own != "default":
|
|
3270
3385
|
found.append(self._bound(own, number))
|
|
3271
3386
|
m = _JS_NAME_IS_RE.match(self.code[index]) if _is_column_0(self.lines[index]) else None
|
|
3272
|
-
if
|
|
3273
|
-
m
|
|
3387
|
+
if (
|
|
3388
|
+
m is not None and (m["var"] or m["type"]) == name and m["value"] != name
|
|
3389
|
+
and (m["end"] or not self._goes_on(index))
|
|
3390
|
+
and _js_passes_parameters(m["params"], m["args"])
|
|
3274
3391
|
):
|
|
3275
|
-
found.append(self._bound(m
|
|
3392
|
+
found.append(self._bound(m["value"], number))
|
|
3393
|
+
m = (
|
|
3394
|
+
_JS_EXPORT_DEFAULT_IS_RE.match(self.code[index])
|
|
3395
|
+
if name == "default" and _is_column_0(self.lines[index]) else None
|
|
3396
|
+
)
|
|
3397
|
+
if m is not None and m["value"] != name and (m["end"] or not self._goes_on(index)):
|
|
3398
|
+
bound = self.binding(m["value"], number) # a namespace import's (None, module) renames nothing
|
|
3399
|
+
if bound is not None:
|
|
3400
|
+
found.append(bound)
|
|
3401
|
+
elif declared_line(self.parts, m["value"], lines=self.lines, cache=self.cache,
|
|
3402
|
+
module_fallback=False) is not None:
|
|
3403
|
+
found.append((m["value"], None))
|
|
3276
3404
|
return [(local, module) for local, module in found if local is not None]
|
|
3277
3405
|
|
|
3406
|
+
def member(self, number: int, name: str) -> tuple[str, str] | None:
|
|
3407
|
+
"""(`L2`, its module) of a column-0 Python `NAME = M.L2` (or
|
|
3408
|
+
`NAME: <type> = M.L2`) on line `number` (from 1) whose `M` a
|
|
3409
|
+
column-0 `from X import M` binds: the module is `X` joined with
|
|
3410
|
+
`M`'s imported name, `.` prefixes kept (`from . import _core` gives
|
|
3411
|
+
`._core`). A step of a rename chain only
|
|
3412
|
+
(`_DeclarationLookup._aliases`): at the documented name's own line
|
|
3413
|
+
it renames nothing. None for any other line, an `M` an
|
|
3414
|
+
`import a.b as M` binds included."""
|
|
3415
|
+
if not self.python or not 0 < number <= len(self.lines):
|
|
3416
|
+
return None
|
|
3417
|
+
if number not in (self.starts or set()) or not _is_column_0(self.lines[number - 1]):
|
|
3418
|
+
return None
|
|
3419
|
+
m = re.match(_PY_NAME_IS_MEMBER.format(name=re.escape(name)), self.code[number - 1])
|
|
3420
|
+
bound = None if m is None else self.binding(m.group(1), number)
|
|
3421
|
+
if bound is None or bound[0] is None or bound[1] is None:
|
|
3422
|
+
return None
|
|
3423
|
+
imported, module = bound
|
|
3424
|
+
return m.group(2), (module if module.endswith(".") else module + ".") + imported
|
|
3425
|
+
|
|
3278
3426
|
|
|
3279
3427
|
class _DeclarationLookup:
|
|
3280
3428
|
"""Where the source declares the stale names (classify-stale's
|
|
@@ -3301,11 +3449,21 @@ class _DeclarationLookup:
|
|
|
3301
3449
|
rules. In the suspects' pass (`keep_declaring`), a file's declarations
|
|
3302
3450
|
are its candidate lines (`_Renames.candidates`: every column-0
|
|
3303
3451
|
declaring line, a Python compatibility import one level in a column-0
|
|
3304
|
-
block and a TS/JS re-export of a
|
|
3305
|
-
the first, and `renamed` then
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
(`_resolve`)
|
|
3452
|
+
block or as a block header's one-line body, and a TS/JS re-export of a
|
|
3453
|
+
renaming import), the file listed at the first, and `renamed` then
|
|
3454
|
+
tells which of a name's candidate lines is an alias of the declaration
|
|
3455
|
+
a cited file holds, from the lines the lookup read, reading too the
|
|
3456
|
+
files a rename's `from` resolves to (`_resolve`) and, along a chain of
|
|
3457
|
+
renames (`_aliases`), the files each later step resolves to. The
|
|
3458
|
+
renames read include an annotated `const`, `let` or `var` or Python
|
|
3459
|
+
`NAME: <type> = L`, a generic `type NAME<P1, ...> = L<P1, ...>`, an
|
|
3460
|
+
import one level in a `with` or `finally` block or as a block header's
|
|
3461
|
+
one-line body, a column-0 `export default L` of an `L` an import or a
|
|
3462
|
+
declaration of the file binds, and a middle step `L = M.L2`; a chain
|
|
3463
|
+
that leaves the resolved files, loops or ends at another declaration,
|
|
3464
|
+
a constrained, defaulted, swapped or instantiated generic, a call and
|
|
3465
|
+
a default export of any other expression stay fabricated.
|
|
3466
|
+
Any error of the filesystem raises (`_LOOKUP_ERRORS`)."""
|
|
3309
3467
|
|
|
3310
3468
|
def __init__(self, entries: list[dict], source_root: Path) -> None:
|
|
3311
3469
|
self.root = source_root
|
|
@@ -3331,6 +3489,8 @@ class _DeclarationLookup:
|
|
|
3331
3489
|
self._candidates: dict[tuple[str, str], list[int]] = {}
|
|
3332
3490
|
# the files a rename's `from` resolves to and the renaming files, that no entry cites
|
|
3333
3491
|
self._resolved: dict[tuple[str, ...], list[str] | None] = {}
|
|
3492
|
+
# the renames of each file a rename chain goes through (`_aliases`), by path
|
|
3493
|
+
self._chain: dict[tuple[str, ...], _Renames | None] = {}
|
|
3334
3494
|
|
|
3335
3495
|
# -- the walk root ------------------------------------------------------
|
|
3336
3496
|
|
|
@@ -3632,7 +3792,9 @@ class _DeclarationLookup:
|
|
|
3632
3792
|
continue
|
|
3633
3793
|
if lines is None:
|
|
3634
3794
|
lines = _text_lines(text)
|
|
3635
|
-
|
|
3795
|
+
# this file's rules only; a declaring file's `_Renames` keeps it
|
|
3796
|
+
# for `renamed`
|
|
3797
|
+
cache = _SourceCache()
|
|
3636
3798
|
path = Path(*parts)
|
|
3637
3799
|
if path.suffix.lower() in PYTHON_EXTENSIONS and (
|
|
3638
3800
|
cache.py_statement_starts(path, lines) is None
|
|
@@ -3651,35 +3813,85 @@ class _DeclarationLookup:
|
|
|
3651
3813
|
self._declaring = {}
|
|
3652
3814
|
self._candidates = {}
|
|
3653
3815
|
|
|
3816
|
+
def _renames_of(self, parts: tuple[str, ...]) -> _Renames | None:
|
|
3817
|
+
"""The renames of a file a rename chain goes through, read once
|
|
3818
|
+
(`_lines_of`); None when the lookup does not read it."""
|
|
3819
|
+
if parts not in self._chain:
|
|
3820
|
+
lines = self._lines_of(parts)
|
|
3821
|
+
self._chain[parts] = None if lines is None else _Renames(parts, lines, self._cited_cache)
|
|
3822
|
+
return self._chain[parts]
|
|
3823
|
+
|
|
3824
|
+
def _steps(self, parts: tuple[str, ...], local: str) -> list[tuple[str, str | None]]:
|
|
3825
|
+
"""The renames of `local` a file a rename chain goes through makes,
|
|
3826
|
+
each a next step (`L2`, its module, None when unknown): per
|
|
3827
|
+
candidate line of `local` in order (`_Renames.candidates`), each
|
|
3828
|
+
rename the line makes (`_Renames.renames`) and a Python
|
|
3829
|
+
`local = M.L2` (`_Renames.member`). Empty when the file renames no
|
|
3830
|
+
`local` or the lookup does not read it."""
|
|
3831
|
+
renames = self._renames_of(parts)
|
|
3832
|
+
steps: list[tuple[str, str | None]] = []
|
|
3833
|
+
for line in [] if renames is None else renames.candidates(local, self._cited_cache, False):
|
|
3834
|
+
steps.extend(renames.renames(line, local))
|
|
3835
|
+
member = renames.member(line, local)
|
|
3836
|
+
if member is not None:
|
|
3837
|
+
steps.append(member)
|
|
3838
|
+
return steps
|
|
3839
|
+
|
|
3654
3840
|
def _aliases(
|
|
3655
3841
|
self, parts: tuple[str, ...], local: str, module: str | None,
|
|
3656
|
-
cited: list[tuple[str, ...]],
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3842
|
+
cited: list[tuple[str, ...]], seen: set[tuple[tuple[str, ...], str]],
|
|
3843
|
+
first: bool = True,
|
|
3844
|
+
) -> str | None:
|
|
3845
|
+
"""The name a cited file declares when a rename in the file `parts`
|
|
3846
|
+
of `local` from `module` (None when unknown) renames that
|
|
3847
|
+
declaration, directly (`local` itself) or through a chain of
|
|
3848
|
+
renames (the name at the chain's end); None when it does not. The
|
|
3849
|
+
files `local` resolves to are the file `parts` when `module` is
|
|
3850
|
+
None, else the files the module resolves to (`_resolve`). When one
|
|
3851
|
+
of them that declares `local` is cited, the rename is an alias.
|
|
3852
|
+
Otherwise the chain follows each of them that declares `local` or
|
|
3853
|
+
renames it (`_steps`: an export-list alias, a compatibility import
|
|
3854
|
+
in a block or on one line, an import then a plain re-export, an
|
|
3855
|
+
assignment of a bare `L`, annotated or not, an `export default L`
|
|
3856
|
+
of a bound `L`, or a Python `local = M.L2`): each rename is the
|
|
3857
|
+
next step, until a step reaches a cited file that declares its
|
|
3858
|
+
name, or none does. When no file declares `local` (a package name,
|
|
3859
|
+
a path that names no file, a file that declares no `local`, such as
|
|
3860
|
+
a barrel that re-exports it), a cited file that declares `local`
|
|
3861
|
+
clears the name at the `first` rename only; at a later step that
|
|
3862
|
+
case ends the chain unless a resolved file renames `local`. `seen`
|
|
3863
|
+
holds the (file, name) pairs the chain followed, each a resolved
|
|
3864
|
+
file that declares or renames the name, fresh for each first
|
|
3865
|
+
rename: a chain that comes back to one (a loop) ends. No module or
|
|
3866
|
+
package so named counts."""
|
|
3867
|
+
resolved = [parts] if module is None else self._resolve(parts, module) if module else []
|
|
3868
|
+
declaring = [p for p in resolved if self._declares(p, local)]
|
|
3869
|
+
if not declaring and first and any(self._declares(p, local) for p in cited):
|
|
3870
|
+
return local
|
|
3871
|
+
if any(p in cited for p in declaring):
|
|
3872
|
+
return local
|
|
3873
|
+
for file in resolved:
|
|
3874
|
+
if (file, local) in seen:
|
|
3875
|
+
continue
|
|
3876
|
+
seen.add((file, local))
|
|
3877
|
+
for name, source in self._steps(file, local):
|
|
3878
|
+
end = self._aliases(file, name, source, cited, seen, first=False)
|
|
3879
|
+
if end is not None:
|
|
3880
|
+
return end
|
|
3881
|
+
return None
|
|
3673
3882
|
|
|
3674
3883
|
def renamed(
|
|
3675
3884
|
self, name: str, hits: list[str], files: list[str]
|
|
3676
3885
|
) -> tuple[str, str] | None:
|
|
3677
3886
|
"""(`file:line`, `L`) of the first rename to `name` that is an alias
|
|
3678
3887
|
of the declaration of `L` a file of `files` (source_file values, as
|
|
3679
|
-
recorded) holds (`_aliases`)
|
|
3888
|
+
recorded) holds (`_aliases`), directly or through a chain of renames
|
|
3889
|
+
across the files it resolves: per file of `hits` (`file:line`
|
|
3680
3890
|
declarations of `name` the lookup found, sorted), per candidate
|
|
3681
3891
|
line in order (`_Renames.candidates`), each rename the line makes
|
|
3682
|
-
(`_Renames.renames`).
|
|
3892
|
+
(`_Renames.renames`). `L` is the name the cited file declares where
|
|
3893
|
+
the chain ends: for a one-step rename, the name that rename
|
|
3894
|
+
renames. The lines are the ones the lookup read after
|
|
3683
3895
|
`keep_declaring` (`_first`), never read again; a file a rename's
|
|
3684
3896
|
module resolves to is read once. None when no line is such an
|
|
3685
3897
|
alias."""
|
|
@@ -3693,8 +3905,9 @@ class _DeclarationLookup:
|
|
|
3693
3905
|
self._resolved.setdefault(renames.parts, renames.lines)
|
|
3694
3906
|
for line in self._candidates.get((file, name), [int(number)]):
|
|
3695
3907
|
for local, module in renames.renames(line, name):
|
|
3696
|
-
|
|
3697
|
-
|
|
3908
|
+
end = self._aliases(renames.parts, local, module, cited, set())
|
|
3909
|
+
if end is not None:
|
|
3910
|
+
return f"{file}:{line}", end
|
|
3698
3911
|
return None
|
|
3699
3912
|
|
|
3700
3913
|
|
|
@@ -3739,15 +3952,28 @@ def classify_stale(
|
|
|
3739
3952
|
no other name), with no line 1 from a cited module or package so named,
|
|
3740
3953
|
and none in the walk unless it is the export, and is `aliased`, not
|
|
3741
3954
|
fabricated, when one of its candidate lines renames to it the
|
|
3742
|
-
declaration a `checked` entry's file holds
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3955
|
+
declaration a `checked` entry's file holds, directly or through a
|
|
3956
|
+
chain of renames across the files it resolves
|
|
3957
|
+
(`_DeclarationLookup.renamed`, which resolves the rename's `from` and
|
|
3958
|
+
follows the chain until it reaches a cited declaration, leaves the
|
|
3959
|
+
files it reads, loops or ends at another declaration), and its
|
|
3960
|
+
`renames` then holds the name the cited file declares where the chain
|
|
3961
|
+
ends, for a one-step rename the name that line renames (no other item
|
|
3962
|
+
has the key); any other such name keeps both null. The renames read
|
|
3963
|
+
include an annotated `const`, `let` or `var` or Python
|
|
3964
|
+
`NAME: <type> = L`, a generic `type NAME<P1, ...> = L<P1, ...>`, an
|
|
3965
|
+
import one level in a `with` or `finally` block or as a block header's
|
|
3966
|
+
one-line body, a column-0 `export default L` of an `L` an import or a
|
|
3967
|
+
declaration of the file binds, and a middle step `L = M.L2`; a chain
|
|
3968
|
+
that leaves the resolved files, loops or ends at another declaration,
|
|
3969
|
+
a constrained, defaulted, swapped or instantiated generic, a call and
|
|
3970
|
+
a default export of any other expression stay fabricated.
|
|
3971
|
+
The classify-stale section of the module docstring gives each reason,
|
|
3972
|
+
what a `no-file` entry means, which entry gives `source` and how
|
|
3973
|
+
`defined_at` and `declared_in` are found. `kinds` (`inventory_kinds`,
|
|
3974
|
+
None without `--inventory`) keeps a name of a `NO_MODULE_KINDS` kind
|
|
3975
|
+
from a module or package so named, unless an entry of the name is a
|
|
3976
|
+
`module` or `package` one.
|
|
3751
3977
|
"""
|
|
3752
3978
|
entries = [e for e in prov.get("entries") or [] if isinstance(e, dict)]
|
|
3753
3979
|
results: list[dict] = []
|
|
@@ -3816,7 +4042,7 @@ def classify_stale(
|
|
|
3816
4042
|
found, declared = {}, {}
|
|
3817
4043
|
# the suspects' own lookup, after: an error there clears no name settled
|
|
3818
4044
|
# above, and keeps every suspect fabricated
|
|
3819
|
-
aliased: dict[str, tuple[str, str]] = {} # name -> (the rename's `file:line`, the `L`
|
|
4045
|
+
aliased: dict[str, tuple[str, str]] = {} # name -> (the rename's `file:line`, the chain-end `L`)
|
|
3820
4046
|
suspect_declared: dict[str, list[str]] = {}
|
|
3821
4047
|
if suspects and lookup is not None:
|
|
3822
4048
|
try:
|