bmad-module-skill-forge 4.0.0 → 6.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/agents.md +1 -1
- package/docs/architecture.md +5 -5
- package/docs/campaign.md +1 -1
- 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/troubleshooting.md +9 -3
- package/docs/verifying-a-skill.md +42 -21
- package/docs/why-skf.md +1 -1
- package/docs/workflows.md +6 -6
- package/package.json +1 -1
- package/src/knowledge/tool-resolution.md +6 -6
- package/src/shared/references/headless-gate-convention.md +4 -1
- package/src/shared/references/output-contract-schema.md +3 -3
- package/src/shared/references/pipeline-contracts.md +2 -1
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.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 +1180 -189
- package/src/shared/scripts/skf-classify-changed-files.py +15 -10
- 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 +64 -20
- package/src/shared/scripts/skf-extract-public-api.py +472 -120
- 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 +136 -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 +574 -45
- package/src/shared/tool-requirements.yaml +8 -2
- package/src/skf-analyze-source/SKILL.md +3 -2
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-analyze-source/references/generate-briefs.md +1 -1
- package/src/skf-analyze-source/references/headless-contract.md +1 -1
- package/src/skf-audit-skill/SKILL.md +4 -3
- package/src/skf-audit-skill/references/re-index.md +2 -2
- package/src/skf-audit-skill/references/report.md +1 -1
- package/src/skf-brief-skill/SKILL.md +3 -2
- package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -2
- package/src/skf-brief-skill/references/invocation-contract.md +2 -2
- package/src/skf-brief-skill/references/scope-definition.md +4 -4
- package/src/skf-campaign/SKILL.md +3 -2
- package/src/skf-campaign/references/campaign-contracts.md +2 -0
- package/src/skf-campaign/references/step-05-skill-loop.md +8 -3
- package/src/skf-campaign/references/step-10-export.md +3 -2
- package/src/skf-create-skill/SKILL.md +4 -3
- package/src/skf-create-skill/assets/skill-sections.md +1 -1
- package/src/skf-create-skill/references/ccc-index-check.md +1 -1
- package/src/skf-create-skill/references/component-extraction.md +1 -1
- package/src/skf-create-skill/references/extract.md +3 -2
- package/src/skf-create-skill/references/extraction-patterns.md +3 -3
- package/src/skf-create-skill/references/generate-artifacts.md +1 -1
- package/src/skf-create-skill/references/report.md +8 -3
- package/src/skf-create-skill/references/sub/ccc-discover.md +1 -1
- package/src/skf-create-skill/references/validate.md +1 -1
- package/src/skf-create-stack-skill/SKILL.md +3 -2
- package/src/skf-create-stack-skill/references/generate-output.md +1 -1
- package/src/skf-create-stack-skill/references/invocation-contract.md +1 -1
- package/src/skf-create-stack-skill/references/parallel-extract.md +2 -2
- package/src/skf-create-stack-skill/references/report.md +1 -1
- package/src/skf-create-stack-skill/references/validate.md +1 -1
- package/src/skf-drop-skill/SKILL.md +3 -2
- package/src/skf-drop-skill/references/invocation-contract.md +1 -1
- package/src/skf-drop-skill/references/report.md +1 -1
- package/src/skf-export-skill/SKILL.md +3 -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/invocation-contract.md +1 -1
- package/src/skf-export-skill/references/load-skill.md +1 -1
- package/src/skf-export-skill/references/summary.md +1 -1
- package/src/skf-export-skill/references/token-report.md +29 -9
- package/src/skf-forger/SKILL.md +3 -2
- package/src/skf-forger/references/pipeline-mode.md +2 -2
- package/src/skf-quick-skill/SKILL.md +4 -3
- package/src/skf-quick-skill/references/finalize.md +1 -1
- package/src/skf-quick-skill/references/halt-contract.md +2 -2
- package/src/skf-quick-skill/references/write-and-validate.md +1 -1
- package/src/skf-refine-architecture/SKILL.md +8 -9
- package/src/skf-refine-architecture/customize.toml +1 -1
- package/src/skf-refine-architecture/references/report.md +1 -1
- package/src/skf-rename-skill/SKILL.md +3 -2
- package/src/skf-rename-skill/references/invocation-contract.md +2 -2
- package/src/skf-setup/SKILL.md +3 -2
- package/src/skf-setup/references/ccc-index.md +1 -1
- package/src/skf-setup/references/invocation-contract.md +1 -1
- package/src/skf-test-skill/SKILL.md +3 -2
- package/src/skf-test-skill/assets/output-section-formats.md +6 -4
- package/src/skf-test-skill/references/coherence-check.md +11 -8
- package/src/skf-test-skill/references/coverage-check-branches.md +81 -0
- package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
- package/src/skf-test-skill/references/coverage-check.md +60 -97
- package/src/skf-test-skill/references/init.md +2 -1
- package/src/skf-test-skill/references/inventory-contract.md +27 -0
- package/src/skf-test-skill/references/invocation-contract.md +3 -3
- package/src/skf-test-skill/references/migration-section-rules.md +26 -14
- package/src/skf-test-skill/references/report.md +5 -5
- package/src/skf-test-skill/references/scoring-rules.md +8 -7
- package/src/skf-test-skill/references/signature-comparison-rules.md +72 -0
- package/src/skf-test-skill/references/source-access-protocol.md +14 -10
- package/src/skf-test-skill/references/step-hard-gate.md +1 -0
- package/src/skf-test-skill/scripts/build-result-context.py +20 -3
- package/src/skf-test-skill/scripts/gap-ledger.py +456 -176
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +516 -79
- package/src/skf-test-skill/scripts/locate-export-segments.py +685 -76
- package/src/skf-test-skill/scripts/reconcile-coverage.py +192 -39
- package/src/skf-test-skill/scripts/score-signatures.py +1205 -65
- package/src/skf-test-skill/scripts/stage-helper-payload.py +64 -13
- package/src/skf-test-skill/scripts/validate-inventory.py +350 -35
- package/src/skf-test-skill/templates/test-report-template.md +1 -0
- package/src/skf-update-skill/SKILL.md +3 -2
- package/src/skf-update-skill/references/detect-changes.md +1 -1
- package/src/skf-update-skill/references/gap-driven.md +54 -33
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- 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 +6 -5
- package/src/skf-update-skill/references/write.md +42 -20
- package/src/skf-verify-stack/SKILL.md +4 -3
- package/tools/cli/lib/tool-check.js +158 -2
|
@@ -350,7 +350,11 @@ scope.type and language; --include, --exclude, --tier-a-include,
|
|
|
350
350
|
Which recipes. --recipe-set picks the recipes whose metadata.sets lists it:
|
|
351
351
|
`component-library` when the scope type is component-library, else
|
|
352
352
|
`standard`. Each recipe runs in the languages its metadata.languages
|
|
353
|
-
lists. A match whose name starts with `_` is never an
|
|
353
|
+
lists. A Python or Rust match whose name starts with `_` is never an
|
|
354
|
+
export, nor public anywhere below: Python's by the Source API Surface
|
|
355
|
+
Definition's rule, Rust's because a `_` item is hidden or unnamed (a
|
|
356
|
+
`#[doc(hidden)] pub mod __private`, which the runner cannot read, or a
|
|
357
|
+
`pub const _`); a TS/JS `_` name is an export like any other. --head-cap
|
|
354
358
|
keeps at most N matches of each recipe, in file and line order (0: no
|
|
355
359
|
cap); without it the cap is 200, or at Forge+ and Deep tiers 500 for a
|
|
356
360
|
full-library scope and 300 for a component-library one. `truncated` is
|
|
@@ -381,7 +385,11 @@ the line cap, say), which the caller then reads by eye. An item of a local `expo
|
|
|
381
385
|
list takes instead the type of the top-level binding it names in its file:
|
|
382
386
|
what the recipes record for that declaration exported in place (a const
|
|
383
387
|
bound to an arrow function or a function expression is a function), or
|
|
384
|
-
`re-export` for an import.
|
|
388
|
+
`re-export` for an import. When that binding is declared in the file (not
|
|
389
|
+
an import or a destructured name), the item also gets `declaration_line`
|
|
390
|
+
and `declaration_signature_line`, its `$NAME` line and the source line
|
|
391
|
+
holding it (an overload set's first signature); its `source_line`,
|
|
392
|
+
`signature_line` and `citation` stay at the list.
|
|
385
393
|
|
|
386
394
|
Entry points. The export surface each package declares: every subpath of a
|
|
387
395
|
package.json `exports` map (committed .d.ts/.d.mts targets outside src/
|
|
@@ -423,40 +431,75 @@ module names.
|
|
|
423
431
|
A chain the trace cannot follow (a module no file here holds, a `#[path]`
|
|
424
432
|
module) is listed in `unresolved`. The diff against the recipe inventory,
|
|
425
433
|
one language family at a time (JavaScript/TypeScript, Python, Rust, Go):
|
|
426
|
-
`public` (the entry points' names
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
434
|
+
`public` (the entry points' names, each at the file and line the first
|
|
435
|
+
entry point naming it gives, in path order, a JS/TS entry point's own and
|
|
436
|
+
named exports all coming before any `export *`, unless that file is out
|
|
437
|
+
of scope, or it gives none, and another entry point gives a file in
|
|
438
|
+
scope, other than through a star or glob its own export of the name
|
|
439
|
+
shadows),
|
|
440
|
+
`other_definitions` (each other definition of a public name in a file in
|
|
441
|
+
scope, one row per file and `local` name: the file and line an entry
|
|
442
|
+
point gives, never a namespace nor one beside a public row that is a
|
|
443
|
+
namespace, nor a star or glob through which an entry point passes on a
|
|
444
|
+
name it already exports (its own export shadows it), while two exports
|
|
445
|
+
of the name by one entry point, such as Rust `pub use` items a `cfg`
|
|
446
|
+
gates, are both kept; in every language family, so a name each
|
|
447
|
+
framework package of a monorepo defines keeps
|
|
448
|
+
each package's file, while a gap and an outside_scope row still go by
|
|
449
|
+
name, from `public` alone),
|
|
450
|
+
`internal` (names the recipes found that no entry point exports or makes
|
|
451
|
+
reachable), `extraction_gaps` (names an entry point exports that no
|
|
452
|
+
recipe found, defined in a file in scope or in none the trace reached:
|
|
453
|
+
read them by eye) and `outside_scope` (names an entry point exports that
|
|
454
|
+
a file out of scope defines, even when a recipe found the name in another
|
|
455
|
+
file, which is another definition: widen scope.include to document
|
|
431
456
|
them). A namespace with `members` is diffed as its members: each is found
|
|
432
457
|
when a recipe found its name, or its `local` one, in its file (one with
|
|
433
458
|
no file by its name alone, as a public name is), and has no row of its
|
|
434
459
|
own when a public name (or another namespace's member) has its name and
|
|
435
460
|
file; one with null `members`, and a member that is such a module, is no
|
|
436
461
|
gap. In a family with no entry point, or only empty ones, every name the
|
|
437
|
-
recipes found is public.
|
|
462
|
+
recipes found is public. A JS/TS module exports, besides the names the
|
|
463
|
+
recipes match, those its column-0 `export` statements declare in a form
|
|
464
|
+
no recipe matches, read from its text: `export let`, `export declare
|
|
465
|
+
const`, `export namespace`, and each name a destructuring pattern that
|
|
466
|
+
opens an `export const`, `let` or `var` statement binds (`export const
|
|
467
|
+
{ a, b: c } = obj` exports a and c), at the line the name is on. Such a
|
|
468
|
+
name that an entry point exports, or passes on through `export *`, is
|
|
469
|
+
public and, no recipe having found it, an `extraction_gaps` row
|
|
470
|
+
(`outside_scope`, in a file out of scope) at that line. A pattern after
|
|
471
|
+
the statement's first declarator (`export const a = 1, { b } = o`), or
|
|
472
|
+
one the reader cannot read (one that does not close, or a default
|
|
473
|
+
holding a template literal over several lines), binds nothing here and
|
|
474
|
+
no warning names it: read it by eye.
|
|
438
475
|
|
|
439
476
|
Counts. `exports_public_api` counts the public names (the package's whole
|
|
440
477
|
public API, `outside_scope` names included), each distinct name and file
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
478
|
+
`public` gives once (`other_definitions` adds none): a namespace with
|
|
479
|
+
`members` counts as its members (none for an empty list, and a member
|
|
480
|
+
that is also a public name, or another namespace's member, once), one
|
|
481
|
+
with null `members` as one. `exports_internal` counts the internal names.
|
|
482
|
+
`effective_denominator` counts those public names with a definition file
|
|
483
|
+
(the `public` row's, or one of its `other_definitions`') that matches
|
|
484
|
+
scope.include (any file when it has no glob), scope.tier_a_include when
|
|
485
|
+
given, and no scope.exclude glob: each name once, however many in-scope
|
|
486
|
+
files define it, so a type counts once whatever its methods, a barrel
|
|
487
|
+
that only re-exports adds nothing of its own, and a name the entry points
|
|
488
|
+
pass on from two files (two packages' entry points that each define it,
|
|
489
|
+
or a namespace member named like another public name, or like another
|
|
490
|
+
namespace's member, at another file) or in two languages counts once,
|
|
491
|
+
when any one of its files matches, as Test Skill's sets count it. A tier
|
|
492
|
+
A list that counts no public name (each glob matches no file, or only
|
|
493
|
+
files scope.exclude drops, that scope.include leaves out or that define
|
|
494
|
+
no public name) narrows nothing on a run that counted a public name and
|
|
495
|
+
read every file (no `errors` item): the count falls back to scope.include
|
|
496
|
+
(else every file), with a warning.
|
|
458
497
|
`arms` gives compile's effective_denominator arms:
|
|
459
|
-
(a) `monorepo` from skf-detect-workspaces.py
|
|
498
|
+
(a) `monorepo` from skf-detect-workspaces.py over the tree the brief
|
|
499
|
+
keeps (its scoped_tree: a member counts only through the files no
|
|
500
|
+
scope.exclude glob matches, so excluding its sources drops it, excluding
|
|
501
|
+
only its manifest does not, root manifests always stay, and a workspace
|
|
502
|
+
with no member left is none), (b) `specific_modules`
|
|
460
503
|
(scope type specific-modules on a repository that is not a monorepo) and
|
|
461
504
|
(c) `multi_subpath_exports` (an in-scope package.json whose `exports` map
|
|
462
505
|
has more than one non-root subpath without a `*`, on a repository that is
|
|
@@ -466,20 +509,22 @@ Store or not. `curated_subset` is true when the files the count's basis
|
|
|
466
509
|
globs list (scope.include's, else every file; a tier A count's, those
|
|
467
510
|
among them a tier A glob matches too; under arm (b), less those a
|
|
468
511
|
scope.exclude glob matches) leave out a file that defines a counted public
|
|
469
|
-
name (an `outside_scope` name's
|
|
470
|
-
|
|
512
|
+
name (an `outside_scope` name's file and an `other_definitions` file
|
|
513
|
+
included) or every source file of a
|
|
514
|
+
workspace skf-detect-workspaces.py finds over the whole tree
|
|
515
|
+
(scope.exclude aside, unlike arm (a): a member the brief drops is still
|
|
516
|
+
part of the repository it chose a subset of); a basis of `all-files` lists
|
|
471
517
|
none. `effective_denominator_decision` says whether create-skill and
|
|
472
518
|
update-skill store `effective_denominator` as stats.effective_denominator,
|
|
473
519
|
the brief's scope.notes aside (documenting the stratification is the one
|
|
474
|
-
judgment left to them): `store` is true only when none of these holds,
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
`
|
|
481
|
-
|
|
482
|
-
deflation).
|
|
520
|
+
judgment left to them): `store` is true only when none of these holds, and
|
|
521
|
+
`reason` names the first that does, else `curated-subset`: `reference-app`
|
|
522
|
+
(scope type reference-app: pattern_surfaces_documented is its basis),
|
|
523
|
+
`incomplete-run` (an `errors` item: a file a failed call left unread can
|
|
524
|
+
hide a public name, so the count can be 0 or partial), `no-arm` (no arm is
|
|
525
|
+
true), `full-library` (scope type full-library), `all-files` (the basis),
|
|
526
|
+
`not-curated` (`curated_subset` false) and `zero-count` (a stored 0 is no
|
|
527
|
+
denominator: Test Skill reads it as deflation).
|
|
483
528
|
|
|
484
529
|
Output JSON (stdout, or -o):
|
|
485
530
|
|
|
@@ -510,7 +555,10 @@ Output JSON (stdout, or -o):
|
|
|
510
555
|
"citation": "[AST:<file>:L<line>]", "ast_recipe",
|
|
511
556
|
"ast_node_type", "export_type", "language",
|
|
512
557
|
"from": "<module>" | null, "from_file": "<file>" | null,
|
|
513
|
-
"confidence": "T1", "extraction_method": "ast-grep"
|
|
558
|
+
"confidence": "T1", "extraction_method": "ast-grep",
|
|
559
|
+
# only on a local export list item whose binding is
|
|
560
|
+
# declared in the file:
|
|
561
|
+
"declaration_line": N, "declaration_signature_line": "<line>"}, ...],
|
|
514
562
|
"aggregates": {"exports": N, "by_type": {"function": N, ...},
|
|
515
563
|
"t1": N, "t1_low": 0},
|
|
516
564
|
"entry_points": {"status": "barrel" | "empty-barrel" | "no-entry-point"
|
|
@@ -530,6 +578,8 @@ Output JSON (stdout, or -o):
|
|
|
530
578
|
"file", "line",
|
|
531
579
|
"members": [{"name", "local", "file", "line"}, ...]
|
|
532
580
|
| null}, ...], # members: via namespace only
|
|
581
|
+
"other_definitions": [{"name", "language", "entry", "via", "from",
|
|
582
|
+
"local", "file", "line"}, ...], # by name, then file
|
|
533
583
|
"internal": [{"name", "language", "source_file", "source_line"}, ...],
|
|
534
584
|
"extraction_gaps": [{"name", "language", "entry", "file", "line"}, ...],
|
|
535
585
|
"outside_scope": [{"name", "language", "entry", "file", "line"}, ...]},
|
|
@@ -618,9 +668,16 @@ the entry points export by name, which counts as the names its module
|
|
|
618
668
|
passes on), each export of a --quick output at its `defined_in` (else its
|
|
619
669
|
`source_file`), and the --extraction runner's `entry_point_diff.public`;
|
|
620
670
|
a payload name replaces a --quick or runner one of its family and name.
|
|
671
|
+
The runner's `other_definitions` count in `effective_denominator` and
|
|
672
|
+
`curated_subset` as full mode counts them, never in `exports_public_api`,
|
|
673
|
+
each only while the record kept for its name names the file of the
|
|
674
|
+
runner's `public` row (a row whose name has no runner row, or one a
|
|
675
|
+
payload or --quick name moves to another file, adds nothing), and none
|
|
676
|
+
beside a namespace with members, as in full mode.
|
|
621
677
|
`exports_internal` counts the names of the payload's `exports` (the
|
|
622
|
-
exports extraction found; `_` names left out
|
|
623
|
-
|
|
678
|
+
exports extraction found; Python, Rust and Dart `_` names left out, those
|
|
679
|
+
of every other language counted) and of the runner's `internal` that no
|
|
680
|
+
public name or member has. `effective_denominator`,
|
|
624
681
|
its basis, `curated_subset` and `effective_denominator_decision` follow
|
|
625
682
|
full mode's rules, over the tree under --source-root, with the scope and
|
|
626
683
|
arms full mode works out there; an --extraction that is `incomplete` is
|
|
@@ -638,10 +695,12 @@ and `arms`.
|
|
|
638
695
|
|
|
639
696
|
`entry_point_diff` replaces a compare by eye, each item {"name",
|
|
640
697
|
"language", "file"}: `public`, the names `exports_public_api` counts (a
|
|
641
|
-
namespace as its `members`); `
|
|
642
|
-
counts
|
|
643
|
-
|
|
644
|
-
|
|
698
|
+
namespace as its `members`); `other_definitions`, the runner's other
|
|
699
|
+
definitions the counts read, by name then file; `internal`, the exports
|
|
700
|
+
`exports_internal` counts; `extraction_gaps`, the public names the
|
|
701
|
+
payload and --quick give that no export found has (the payload's
|
|
702
|
+
`exports`, the --extraction runner's names), to trace (the runner lists
|
|
703
|
+
its own gaps).
|
|
645
704
|
|
|
646
705
|
Output JSON: `mode` "denominator", `source_root`, `scope` (as full mode
|
|
647
706
|
writes it), `files_in_scope`, `files_without_recipes`, `entry_points`,
|
|
@@ -1957,18 +2016,22 @@ def _binding_rules(language: str) -> list[dict]:
|
|
|
1957
2016
|
return rules + [r for r in _trace_rules(language) if r["id"] in IMPORT_RULES]
|
|
1958
2017
|
|
|
1959
2018
|
|
|
1960
|
-
def
|
|
1961
|
-
"""(file, name) -> the
|
|
1962
|
-
|
|
2019
|
+
def _bindings(matches: list[dict]) -> dict[tuple[str, str], dict]:
|
|
2020
|
+
"""(file, name) -> the top-level binding `name` has in that file, from
|
|
2021
|
+
the matches of _binding_rules: its form, the export type BINDING_TYPES
|
|
2022
|
+
gives it (`type`), and the `line` and `signature_line` of its match. Of
|
|
2023
|
+
several matches of the winning form (an overload set's signatures), the
|
|
2024
|
+
lowest line wins, the line merge_exports keeps for a name."""
|
|
1963
2025
|
rank = list(BINDING_TYPES)
|
|
1964
|
-
best: dict[tuple[str, str],
|
|
2026
|
+
best: dict[tuple[str, str], tuple[tuple[int, int], dict]] = {}
|
|
1965
2027
|
for m in matches:
|
|
1966
2028
|
form = "import" if m["rule"] in IMPORT_RULES else m["rule"].removeprefix(f"{BINDING_RULE}-")
|
|
1967
2029
|
if form in BINDING_TYPES and m["name"]:
|
|
1968
|
-
key = (m["file"], m["name"])
|
|
1969
|
-
if key not in best or
|
|
1970
|
-
best[key] = form
|
|
1971
|
-
|
|
2030
|
+
key, order = (m["file"], m["name"]), (rank.index(form), m["line"])
|
|
2031
|
+
if key not in best or order < best[key][0]:
|
|
2032
|
+
best[key] = (order, {"form": form, "type": BINDING_TYPES[form], "line": m["line"],
|
|
2033
|
+
"signature_line": m["signature_line"]})
|
|
2034
|
+
return {key: binding for key, (_, binding) in best.items()}
|
|
1972
2035
|
|
|
1973
2036
|
|
|
1974
2037
|
# --------------------------------------------------------------------------
|
|
@@ -2627,11 +2690,22 @@ def _preferred(at_line: list[dict], forms: dict, order: dict[str, int]) -> dict:
|
|
|
2627
2690
|
return min(left, key=lambda m: (order[m["rule"]], m["language"]))
|
|
2628
2691
|
|
|
2629
2692
|
|
|
2693
|
+
def _private(family: str | None, name: str) -> bool:
|
|
2694
|
+
"""A name full mode and denominator mode never count: one that starts
|
|
2695
|
+
with `_` in Python (the Source API Surface Definition leaves it out),
|
|
2696
|
+
Rust (a `#[doc(hidden)] pub mod __private`, which the runner cannot
|
|
2697
|
+
read, or a `pub const _`) or Dart (private to its library). A TS/JS `_`
|
|
2698
|
+
name is exported like any other, and Go's recipe reads only a
|
|
2699
|
+
capitalized name. Quick mode drops only Python's `_` names."""
|
|
2700
|
+
return family in ("python", "rust", "dart") and name.startswith("_")
|
|
2701
|
+
|
|
2702
|
+
|
|
2630
2703
|
def merge_exports(matches: list[dict], recipes: list[dict], recipe_set: str,
|
|
2631
2704
|
head_cap: int | None) -> tuple[list[dict], list[dict]]:
|
|
2632
2705
|
"""(exports, per-recipe stats) from the matches of one recipe set: at
|
|
2633
2706
|
most `head_cap` matches of each recipe, in file and line order, merged
|
|
2634
|
-
by (file, name) at the first line that names it.
|
|
2707
|
+
by (file, name) at the first line that names it. A Python or Rust `_`
|
|
2708
|
+
name is no export."""
|
|
2635
2709
|
forms = _forms(recipes)
|
|
2636
2710
|
order: dict[str, int] = {}
|
|
2637
2711
|
languages: dict[str, list[str]] = {}
|
|
@@ -2642,7 +2716,8 @@ def merge_exports(matches: list[dict], recipes: list[dict], recipe_set: str,
|
|
|
2642
2716
|
langs.extend(lang for lang in recipe["metadata"]["languages"] if lang not in langs)
|
|
2643
2717
|
per_recipe: dict[str, list[dict]] = {rid: [] for rid in order}
|
|
2644
2718
|
for m in matches:
|
|
2645
|
-
if m["rule"] in per_recipe and m["name"] and not
|
|
2719
|
+
if (m["rule"] in per_recipe and m["name"] and not _private(FAMILY_OF.get(m["language"]), m["name"])
|
|
2720
|
+
and (m["rule"], m["language"]) in forms):
|
|
2646
2721
|
per_recipe[m["rule"]].append(m)
|
|
2647
2722
|
stats, kept = [], []
|
|
2648
2723
|
for rid, found in per_recipe.items():
|
|
@@ -2679,7 +2754,10 @@ def _type_list_items(exe: str, root: Path, folder: Path, matches: list[dict], ex
|
|
|
2679
2754
|
export_specifier with no `from`) the type of the top-level binding it
|
|
2680
2755
|
names in its file, as BINDING_TYPES has it; the recipe's own
|
|
2681
2756
|
export_type stays when no such binding has the name (a destructured
|
|
2682
|
-
one).
|
|
2757
|
+
one). A binding declared in the file (not an import) also gives the
|
|
2758
|
+
item `declaration_line` and `declaration_signature_line`, its line and
|
|
2759
|
+
source line; the item's own `source_line`, `signature_line` and
|
|
2760
|
+
citation stay at the list."""
|
|
2683
2761
|
items = [e for e in exports if e["ast_node_type"] == "export_specifier" and e["from"] is None]
|
|
2684
2762
|
if not items:
|
|
2685
2763
|
return
|
|
@@ -2691,10 +2769,16 @@ def _type_list_items(exe: str, root: Path, folder: Path, matches: list[dict], ex
|
|
|
2691
2769
|
languages = sorted({lang for f in files for lang in _rule_languages(_file_language(f) or "")}
|
|
2692
2770
|
& set(RECIPE_LANGUAGES))
|
|
2693
2771
|
rules = [rule for language in languages for rule in _binding_rules(language)]
|
|
2694
|
-
|
|
2772
|
+
bindings = _bindings(_scan(exe, root, folder, rules, files, errors, clock))
|
|
2695
2773
|
for e in items:
|
|
2696
2774
|
name = local.get((e["source_file"], e["ast_recipe"], e["export_name"]), e["export_name"])
|
|
2697
|
-
|
|
2775
|
+
binding = bindings.get((e["source_file"], name))
|
|
2776
|
+
if binding is None:
|
|
2777
|
+
continue
|
|
2778
|
+
e["export_type"] = binding["type"]
|
|
2779
|
+
if binding["form"] != "import":
|
|
2780
|
+
e["declaration_line"] = binding["line"]
|
|
2781
|
+
e["declaration_signature_line"] = binding["signature_line"]
|
|
2698
2782
|
|
|
2699
2783
|
|
|
2700
2784
|
# --------------------------------------------------------------------------
|
|
@@ -2839,14 +2923,190 @@ _JS_DEFAULT_NAME_RE = re.compile(r"^export\s+default\s+([A-Za-z_$][\w$]*)\s*;?\s
|
|
|
2839
2923
|
_JS_DEFAULT_EXPORT_RE = re.compile(r"export\s+default\b")
|
|
2840
2924
|
|
|
2841
2925
|
|
|
2926
|
+
# Keep identical to _js_pattern_bindings in skf-verify-provenance-completeness.py.
|
|
2927
|
+
def _js_pattern_bindings(
|
|
2928
|
+
text: str, start: int
|
|
2929
|
+
) -> tuple[list[tuple[str, int]], int | None]:
|
|
2930
|
+
"""The names the destructuring pattern whose `{` or `[` is at
|
|
2931
|
+
`text[start]` binds, each with its offset in `text`, and the offset
|
|
2932
|
+
just past the pattern's closing bracket.
|
|
2933
|
+
|
|
2934
|
+
An object pattern's property is `...target`, a shorthand key or
|
|
2935
|
+
`key: target` (a key is a name, a string, a number or a computed
|
|
2936
|
+
`[...]`, and only a shorthand key binds itself); an array pattern's
|
|
2937
|
+
element is `...target`, a target or a hole; a target is a name or a
|
|
2938
|
+
nested pattern. A `= default` is skipped to the next comma at the
|
|
2939
|
+
pattern's depth, and so is a property or element of any other shape,
|
|
2940
|
+
which binds nothing. '...', "..." and `...` strings are skipped
|
|
2941
|
+
whole, so no bracket or comma inside one counts. Whitespace, newlines
|
|
2942
|
+
included, may sit anywhere between the parts.
|
|
2943
|
+
|
|
2944
|
+
Returns ([], None) when `text[start]` opens no pattern, the pattern
|
|
2945
|
+
does not close in `text`, a string in it does not end, or a bracket in
|
|
2946
|
+
it closes one it did not open. A pure function of its arguments (no
|
|
2947
|
+
module state), so another script can carry a copy of it.
|
|
2948
|
+
"""
|
|
2949
|
+
n = len(text)
|
|
2950
|
+
names: list[tuple[str, int]] = []
|
|
2951
|
+
|
|
2952
|
+
def space(i: int) -> int:
|
|
2953
|
+
while i < n and text[i].isspace():
|
|
2954
|
+
i += 1
|
|
2955
|
+
return i
|
|
2956
|
+
|
|
2957
|
+
def name_end(i: int) -> int:
|
|
2958
|
+
"""The end of the identifier at `i`, `i` itself when none starts there."""
|
|
2959
|
+
if i < n and (text[i].isalpha() or text[i] in "_$"):
|
|
2960
|
+
i += 1
|
|
2961
|
+
while i < n and (text[i].isalnum() or text[i] in "_$"):
|
|
2962
|
+
i += 1
|
|
2963
|
+
return i
|
|
2964
|
+
|
|
2965
|
+
def string_end(i: int) -> int | None:
|
|
2966
|
+
"""The offset past the string whose quote is at `i`."""
|
|
2967
|
+
quote = text[i]
|
|
2968
|
+
i += 1
|
|
2969
|
+
while i < n:
|
|
2970
|
+
if text[i] == "\\":
|
|
2971
|
+
i += 2
|
|
2972
|
+
elif text[i] == quote:
|
|
2973
|
+
return i + 1
|
|
2974
|
+
else:
|
|
2975
|
+
i += 1
|
|
2976
|
+
return None
|
|
2977
|
+
|
|
2978
|
+
def skip(i: int, to_comma: bool = True) -> int | None:
|
|
2979
|
+
"""From `i`, the offset of the next comma (unless not `to_comma`)
|
|
2980
|
+
or closing bracket at depth 0; with `to_comma` False, `text[i]`
|
|
2981
|
+
opens a bracket and the offset past the one closing it."""
|
|
2982
|
+
depth = 0
|
|
2983
|
+
while i < n:
|
|
2984
|
+
ch = text[i]
|
|
2985
|
+
if ch in "'\"`":
|
|
2986
|
+
end = string_end(i)
|
|
2987
|
+
if end is None:
|
|
2988
|
+
return None
|
|
2989
|
+
i = end
|
|
2990
|
+
continue
|
|
2991
|
+
if ch in "([{":
|
|
2992
|
+
depth += 1
|
|
2993
|
+
elif ch in ")]}":
|
|
2994
|
+
if depth == 0:
|
|
2995
|
+
return i
|
|
2996
|
+
depth -= 1
|
|
2997
|
+
if depth == 0 and not to_comma:
|
|
2998
|
+
return i + 1
|
|
2999
|
+
elif ch == "," and depth == 0 and to_comma:
|
|
3000
|
+
return i
|
|
3001
|
+
i += 1
|
|
3002
|
+
return None
|
|
3003
|
+
|
|
3004
|
+
def default(i: int) -> int | None:
|
|
3005
|
+
"""After a target: past its `= default`, when it has one."""
|
|
3006
|
+
i = space(i)
|
|
3007
|
+
if i < n and text[i] == "=" and text[i + 1 : i + 2] not in ("=", ">"):
|
|
3008
|
+
return skip(i + 1)
|
|
3009
|
+
return i
|
|
3010
|
+
|
|
3011
|
+
def bind(i: int, end: int, close: str) -> int | None:
|
|
3012
|
+
"""The name `text[i:end]` is bound when its property or element
|
|
3013
|
+
ends after it or takes a default (any other text: another shape)."""
|
|
3014
|
+
after = space(end)
|
|
3015
|
+
if after < n and (
|
|
3016
|
+
text[after] in (",", close)
|
|
3017
|
+
or text[after] == "=" and text[after + 1 : after + 2] not in ("=", ">")
|
|
3018
|
+
):
|
|
3019
|
+
names.append((text[i:end], i))
|
|
3020
|
+
return default(after)
|
|
3021
|
+
return after
|
|
3022
|
+
|
|
3023
|
+
def target(i: int, close: str) -> int | None:
|
|
3024
|
+
if i < n and text[i] in "{[":
|
|
3025
|
+
end = pattern(i)
|
|
3026
|
+
return None if end is None else default(end)
|
|
3027
|
+
end = name_end(i)
|
|
3028
|
+
return i if end == i else bind(i, end, close)
|
|
3029
|
+
|
|
3030
|
+
def element(i: int, close: str) -> int | None:
|
|
3031
|
+
"""One property or element at `i`: the offset after it."""
|
|
3032
|
+
if text.startswith("...", i):
|
|
3033
|
+
return target(space(i + 3), close)
|
|
3034
|
+
if close == "]":
|
|
3035
|
+
return target(i, close)
|
|
3036
|
+
if text[i] in "'\"":
|
|
3037
|
+
end = string_end(i)
|
|
3038
|
+
elif text[i] == "[":
|
|
3039
|
+
end = skip(i, to_comma=False) # a computed key
|
|
3040
|
+
else:
|
|
3041
|
+
end = name_end(i)
|
|
3042
|
+
if end > i:
|
|
3043
|
+
after = space(end)
|
|
3044
|
+
if after < n and text[after] == ":":
|
|
3045
|
+
return target(space(after + 1), close)
|
|
3046
|
+
return bind(i, end, close) # a shorthand key
|
|
3047
|
+
while end < n and (text[end].isalnum() or text[end] in "._"):
|
|
3048
|
+
end += 1 # a number key
|
|
3049
|
+
if end is None:
|
|
3050
|
+
return None
|
|
3051
|
+
after = space(end)
|
|
3052
|
+
if end > i and after < n and text[after] == ":":
|
|
3053
|
+
return target(space(after + 1), close)
|
|
3054
|
+
return after
|
|
3055
|
+
|
|
3056
|
+
def pattern(i: int) -> int | None:
|
|
3057
|
+
"""The pattern opening at `i`: the offset past its closing bracket."""
|
|
3058
|
+
close = "}" if text[i] == "{" else "]"
|
|
3059
|
+
i += 1
|
|
3060
|
+
while True:
|
|
3061
|
+
i = space(i)
|
|
3062
|
+
if i >= n:
|
|
3063
|
+
return None
|
|
3064
|
+
if text[i] == close:
|
|
3065
|
+
return i + 1
|
|
3066
|
+
if text[i] in ")]}":
|
|
3067
|
+
return None
|
|
3068
|
+
if text[i] == ",":
|
|
3069
|
+
i += 1 # a hole, or a stray comma
|
|
3070
|
+
continue
|
|
3071
|
+
end = element(i, close)
|
|
3072
|
+
if end is None:
|
|
3073
|
+
return None
|
|
3074
|
+
end = space(end)
|
|
3075
|
+
if end < n and text[end] not in (",", close):
|
|
3076
|
+
end = skip(end) # any other shape binds nothing
|
|
3077
|
+
if end is None:
|
|
3078
|
+
return None
|
|
3079
|
+
i = end + 1 if end < n and text[end] == "," else end
|
|
3080
|
+
|
|
3081
|
+
if not (0 <= start < n and text[start] in "{["):
|
|
3082
|
+
return [], None
|
|
3083
|
+
end = pattern(start)
|
|
3084
|
+
return (names, end) if end is not None else ([], None)
|
|
3085
|
+
|
|
3086
|
+
|
|
2842
3087
|
def _js_text_names(text: str) -> tuple[list[tuple[str, int, bool]], list[tuple[str, int]]]:
|
|
2843
3088
|
"""The names a JS/TS file's column-0 `export` declarations bind, read
|
|
2844
3089
|
from its text for the forms no recipe matches (`export let`, `export
|
|
2845
|
-
declare const`, `export namespace
|
|
2846
|
-
|
|
3090
|
+
declare const`, `export namespace`, and each name a destructuring
|
|
3091
|
+
pattern that opens an `export const`, `let` or `var` statement binds:
|
|
3092
|
+
`export const { a, b: c, ...d } = obj` binds a, c and d), as (name,
|
|
3093
|
+
line, is the default export), and its `export default <name>;`
|
|
3094
|
+
statements, as (name, line).
|
|
3095
|
+
|
|
3096
|
+
A pattern is read from its `{` or `[` on (`_JS_DESTRUCTURE_RE`) with
|
|
3097
|
+
`_js_pattern_bindings`, so it may span lines, and each name it binds
|
|
3098
|
+
takes the line the name is on. A pattern after the first declarator
|
|
3099
|
+
is not read, and one that does not close, or that nests deeper than
|
|
3100
|
+
the interpreter's recursion limit, binds nothing."""
|
|
2847
3101
|
code = _blank(text, "js")
|
|
2848
3102
|
declared = [(m.group(2), _line_of(code, m.start(2)), bool(m.group(1)))
|
|
2849
3103
|
for rx in (_JS_DECLARATION_RE, _JS_IMPORT_EQUALS_RE) for m in rx.finditer(code)]
|
|
3104
|
+
for m in _JS_DESTRUCTURE_RE.finditer(code):
|
|
3105
|
+
try:
|
|
3106
|
+
names, _ = _js_pattern_bindings(code, m.end() - 1)
|
|
3107
|
+
except RecursionError:
|
|
3108
|
+
continue # patterns nested past the interpreter's limit: none bound
|
|
3109
|
+
declared += [(name, _line_of(code, offset), False) for name, offset in names]
|
|
2850
3110
|
defaults = [(m.group(1), _line_of(code, m.start(1))) for m in _JS_DEFAULT_NAME_RE.finditer(code)]
|
|
2851
3111
|
return declared, defaults
|
|
2852
3112
|
|
|
@@ -3084,12 +3344,13 @@ def _member(name: str, local: str | None, file: str | None, line: int | None) ->
|
|
|
3084
3344
|
return {"name": name, "local": local if local and local != name else None, "file": file, "line": line}
|
|
3085
3345
|
|
|
3086
3346
|
|
|
3087
|
-
def _flatten(start, direct: Callable[[object], list[tuple[str, str, object]]]) -> list[dict]:
|
|
3088
|
-
"""The members of the namespace module `start
|
|
3089
|
-
passes on, (name, "member", member) or
|
|
3090
|
-
each nested namespace module's members
|
|
3091
|
-
module once, so a cycle is cut), each
|
|
3092
|
-
that starts with `_`,
|
|
3347
|
+
def _flatten(start, direct: Callable[[object], list[tuple[str, str, object]]], family: str) -> list[dict]:
|
|
3348
|
+
"""The members of the namespace module `start`, of language family
|
|
3349
|
+
`family`: what `direct` says it passes on, (name, "member", member) or
|
|
3350
|
+
(name, "module", the module), each nested namespace module's members
|
|
3351
|
+
spliced in its place (each module once, so a cycle is cut), each
|
|
3352
|
+
(name, file) once, and no Python or Rust name that starts with `_`,
|
|
3353
|
+
which is never public."""
|
|
3093
3354
|
out: list[dict] = []
|
|
3094
3355
|
seen: set[tuple[str, str | None]] = set()
|
|
3095
3356
|
visited: set = set()
|
|
@@ -3097,7 +3358,7 @@ def _flatten(start, direct: Callable[[object], list[tuple[str, str, object]]]) -
|
|
|
3097
3358
|
def walk(module) -> None:
|
|
3098
3359
|
visited.add(module)
|
|
3099
3360
|
for name, kind, value in direct(module):
|
|
3100
|
-
if not name or name
|
|
3361
|
+
if not name or _private(family, name):
|
|
3101
3362
|
continue
|
|
3102
3363
|
if kind == "module":
|
|
3103
3364
|
if value not in visited:
|
|
@@ -3111,17 +3372,35 @@ def _flatten(start, direct: Callable[[object], list[tuple[str, str, object]]]) -
|
|
|
3111
3372
|
|
|
3112
3373
|
|
|
3113
3374
|
class _Surface:
|
|
3114
|
-
"""The public names
|
|
3115
|
-
{name: info}, the first
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3375
|
+
"""The public names the entry points of language family `family`
|
|
3376
|
+
declare: {name: info}, the first `add` of a name wins (the JS/TS trace
|
|
3377
|
+
adds every entry point's own and named exports before any `export *`),
|
|
3378
|
+
unless the file it gives is out of scope (or it gives none) and another
|
|
3379
|
+
entry point gives a file in `in_scope`, which then replaces it (one
|
|
3380
|
+
entry point never replaces its own, and a star or glob that passes on a
|
|
3381
|
+
name its entry point already exports replaces none: an explicit export
|
|
3382
|
+
shadows it; with no `in_scope`, the first always wins); `others`,
|
|
3383
|
+
{name: [info]}, the other definitions of a held name in files in
|
|
3384
|
+
`in_scope` (#830), each an add that does not become the held record,
|
|
3385
|
+
is no such shadowed star or glob, is no namespace, beside a held record
|
|
3386
|
+
that is none, and gives a file in scope whose (file, `local` or name)
|
|
3387
|
+
neither the held record nor an earlier one of `others` gives; the
|
|
3388
|
+
names, and the files, a namespace export (a module) makes reachable,
|
|
3389
|
+
which are not internal; and the re-exports the trace could not follow.
|
|
3390
|
+
A namespace item also carries its `members`, the names its module
|
|
3391
|
+
passes on (None when the trace cannot read that module); `opaque` holds
|
|
3392
|
+
the (name, file) of each member that is itself a module the trace
|
|
3393
|
+
cannot read, which, as such a namespace, is no recipe's target. A
|
|
3394
|
+
Python or Rust `_` name is never added."""
|
|
3395
|
+
|
|
3396
|
+
def __init__(self, family: str, in_scope: set[str] | None = None) -> None:
|
|
3397
|
+
self.family = family
|
|
3398
|
+
self.in_scope = in_scope
|
|
3124
3399
|
self.names: dict[str, dict] = {}
|
|
3400
|
+
self.others: dict[str, list[dict]] = {}
|
|
3401
|
+
self._other_at: dict[str, set[tuple[str, str]]] = {} # name: the (file, local or name) of its others
|
|
3402
|
+
# (entry, name) of each add: a star or glob an entry point passes an exported name on through is shadowed
|
|
3403
|
+
self._added: set[tuple[str, str]] = set()
|
|
3125
3404
|
self.reachable: set[str] = set()
|
|
3126
3405
|
self.reachable_files: set[str] = set()
|
|
3127
3406
|
self.unresolved: list[dict] = []
|
|
@@ -3130,14 +3409,25 @@ class _Surface:
|
|
|
3130
3409
|
def add(self, name: str, entry: str, via: str, file: str | None, line: int | None,
|
|
3131
3410
|
source: str | None = None, local: str | None = None,
|
|
3132
3411
|
members: list[dict] | None = None) -> dict | None:
|
|
3133
|
-
if not name or name
|
|
3412
|
+
if not name or _private(self.family, name):
|
|
3134
3413
|
return None
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3414
|
+
held = self.names.get(name)
|
|
3415
|
+
shadowed = via == "star" and (entry, name) in self._added
|
|
3416
|
+
self._added.add((entry, name))
|
|
3417
|
+
info = {"name": name, "entry": entry, "via": via, "from": source,
|
|
3418
|
+
"local": local if local and local != name else None, "file": file, "line": line}
|
|
3419
|
+
at = (file, info["local"] or name)
|
|
3420
|
+
if held is None or (not shadowed and self.in_scope is not None and file in self.in_scope
|
|
3421
|
+
and entry != held["entry"]
|
|
3422
|
+
and (held["file"] is None or held["file"] not in self.in_scope)):
|
|
3423
|
+
self.names[name] = info
|
|
3139
3424
|
if via == "namespace":
|
|
3140
3425
|
self.names[name]["members"] = [dict(m) for m in members] if members is not None else None
|
|
3426
|
+
elif (not shadowed and via != "namespace" and held["via"] != "namespace" and self.in_scope is not None
|
|
3427
|
+
and file in self.in_scope and at != (held["file"], held["local"] or name)
|
|
3428
|
+
and at not in self._other_at.get(name, ())):
|
|
3429
|
+
self.others.setdefault(name, []).append(info)
|
|
3430
|
+
self._other_at.setdefault(name, set()).add(at)
|
|
3141
3431
|
return self.names[name]
|
|
3142
3432
|
|
|
3143
3433
|
def unresolve(self, entry: str, name: str, source: str | None, file: str) -> None:
|
|
@@ -3341,7 +3631,7 @@ def _js_surface(root: Path, entries: list[dict], tree_set: set[str], forms: dict
|
|
|
3341
3631
|
|
|
3342
3632
|
def members(file: str) -> list[dict]:
|
|
3343
3633
|
if file not in member_lists:
|
|
3344
|
-
member_lists[file] = _flatten(file, direct)
|
|
3634
|
+
member_lists[file] = _flatten(file, direct, "javascript")
|
|
3345
3635
|
return member_lists[file]
|
|
3346
3636
|
|
|
3347
3637
|
def publish(entry: str, name: str, via: str, module: str, source: str | None) -> None:
|
|
@@ -3712,7 +4002,7 @@ def _py_surface(root: Path, entries: list[dict], tree_set: set[str], surface: _S
|
|
|
3712
4002
|
|
|
3713
4003
|
def members(rel: str, top: str) -> list[dict]:
|
|
3714
4004
|
if (rel, top) not in member_lists:
|
|
3715
|
-
member_lists[(rel, top)] = _flatten(rel, lambda module: direct(module, top))
|
|
4005
|
+
member_lists[(rel, top)] = _flatten(rel, lambda module: direct(module, top), "python")
|
|
3716
4006
|
return member_lists[(rel, top)]
|
|
3717
4007
|
|
|
3718
4008
|
for entry in [e["file"] for e in entries]:
|
|
@@ -4030,7 +4320,7 @@ def _rust_surface(root: Path, entries: list[dict], tree_set: set[str], surface:
|
|
|
4030
4320
|
return None
|
|
4031
4321
|
key = (lib, tuple(here))
|
|
4032
4322
|
if key not in member_lists:
|
|
4033
|
-
member_lists[key] = _flatten(tuple(here), lambda path: direct(lib, path))
|
|
4323
|
+
member_lists[key] = _flatten(tuple(here), lambda path: direct(lib, path), "rust")
|
|
4034
4324
|
return member_lists[key]
|
|
4035
4325
|
|
|
4036
4326
|
def publish(lib: str, here: list[str], name: str, via: str, source: str | None,
|
|
@@ -4130,19 +4420,25 @@ def _go_surface(root: Path, files: list[str], surface: _Surface, entries: list[d
|
|
|
4130
4420
|
|
|
4131
4421
|
def entry_point_diff(exports: list[dict], surfaces: dict[str, _Surface], entries: list[dict],
|
|
4132
4422
|
in_scope: set[str]) -> tuple[dict[str, str], dict[tuple[str, str], dict], list[dict],
|
|
4133
|
-
list[tuple[str, dict]], list[tuple[str, dict]]
|
|
4423
|
+
list[tuple[str, dict]], list[tuple[str, dict]],
|
|
4424
|
+
list[tuple[str, dict]]]:
|
|
4134
4425
|
"""The entry-point diff, one language family at a time: (status by
|
|
4135
4426
|
family, public names {(family, name): info}, internal export records,
|
|
4136
|
-
and (family, info) of the extraction gaps
|
|
4137
|
-
|
|
4427
|
+
and (family, info) of the extraction gaps, of the names outside scope
|
|
4428
|
+
and of the other in-scope definitions of public names, `_Surface`'s
|
|
4429
|
+
`others`, which no gap or outside_scope row reads: those go by name).
|
|
4430
|
+
A family whose entry points declare names diffs them with its
|
|
4138
4431
|
exports: an export no entry point names, nor traces back to, nor makes
|
|
4139
4432
|
reachable (a name, or any export of a reachable file), is internal. A
|
|
4140
|
-
declared name
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
recipe found
|
|
4145
|
-
|
|
4433
|
+
declared name, at the file `_Surface` kept for it (not always the first
|
|
4434
|
+
entry point's in path order), is outside scope when a file out of scope
|
|
4435
|
+
defines it, even when a recipe found its name in another file (another
|
|
4436
|
+
definition: no recipe reads a file out of scope), and a gap when no
|
|
4437
|
+
recipe found it and a file in scope defines it (or the trace found no
|
|
4438
|
+
file); a module is no recipe's target, so neither: a namespace with
|
|
4439
|
+
`members` has each member diffed in its place, found when a recipe
|
|
4440
|
+
found its name, or its `local` one, in its file, and given no row of
|
|
4441
|
+
its own when a declared name has its name and file (or another
|
|
4146
4442
|
namespace gave it one); one whose members are unknown (None), or a
|
|
4147
4443
|
member that is such a module (`opaque`), is no gap. In a family with
|
|
4148
4444
|
no entry point, or only empty ones, every export is public."""
|
|
@@ -4151,6 +4447,7 @@ def entry_point_diff(exports: list[dict], surfaces: dict[str, _Surface], entries
|
|
|
4151
4447
|
internal: list[dict] = []
|
|
4152
4448
|
gaps: list[tuple[str, dict]] = []
|
|
4153
4449
|
outside: list[tuple[str, dict]] = []
|
|
4450
|
+
others: list[tuple[str, dict]] = []
|
|
4154
4451
|
entry_families = {FAMILY_OF[e["language"]] for e in entries}
|
|
4155
4452
|
families = {FAMILY_OF[e["language"]] for e in exports} | entry_families
|
|
4156
4453
|
for family in sorted(families | {f for f, s in surfaces.items() if s.names}):
|
|
@@ -4168,6 +4465,7 @@ def entry_point_diff(exports: list[dict], surfaces: dict[str, _Surface], entries
|
|
|
4168
4465
|
names = {e["export_name"] for e in records}
|
|
4169
4466
|
traced = {info["local"] for info in declared.values() if info["local"]} | surface.reachable
|
|
4170
4467
|
public.update(((family, name), info) for name, info in declared.items())
|
|
4468
|
+
others.extend((family, dict(info)) for name in sorted(surface.others) for info in surface.others[name])
|
|
4171
4469
|
seen: set[str] = set()
|
|
4172
4470
|
for e in records:
|
|
4173
4471
|
name = e["export_name"]
|
|
@@ -4176,9 +4474,13 @@ def entry_point_diff(exports: list[dict], surfaces: dict[str, _Surface], entries
|
|
|
4176
4474
|
seen.add(name)
|
|
4177
4475
|
internal.append(e)
|
|
4178
4476
|
for name, info in declared.items():
|
|
4179
|
-
if
|
|
4477
|
+
if info["via"] == "namespace":
|
|
4180
4478
|
continue
|
|
4181
|
-
|
|
4479
|
+
# A recipe never reads a file out of scope: a name it found elsewhere is another definition.
|
|
4480
|
+
if info["file"] is not None and info["file"] not in in_scope:
|
|
4481
|
+
outside.append((family, info))
|
|
4482
|
+
elif name not in names and (info["local"] or name) not in names:
|
|
4483
|
+
gaps.append((family, info))
|
|
4182
4484
|
# A namespace's members, each by its name (or `local`) and file, or
|
|
4183
4485
|
# by name alone when it has no file, as a declared name is: a name
|
|
4184
4486
|
# at a file a declared name already gives counts once.
|
|
@@ -4195,7 +4497,7 @@ def entry_point_diff(exports: list[dict], surfaces: dict[str, _Surface], entries
|
|
|
4195
4497
|
row = {"name": member["name"], "entry": info["entry"], "file": member["file"],
|
|
4196
4498
|
"line": member["line"]}
|
|
4197
4499
|
(gaps if member["file"] is None or member["file"] in in_scope else outside).append((family, row))
|
|
4198
|
-
return statuses, public, internal, gaps, outside
|
|
4500
|
+
return statuses, public, internal, gaps, outside, others
|
|
4199
4501
|
|
|
4200
4502
|
|
|
4201
4503
|
def _has_members(info: dict) -> bool:
|
|
@@ -4204,9 +4506,9 @@ def _has_members(info: dict) -> bool:
|
|
|
4204
4506
|
|
|
4205
4507
|
|
|
4206
4508
|
def _public_keys(public: dict[tuple[str, str], dict]) -> set[tuple[str, str, str | None]]:
|
|
4207
|
-
"""The (family, name, file)
|
|
4208
|
-
namespace with members, in its place each member, and so a member
|
|
4209
|
-
is also a declared name (or another namespace's member) once."""
|
|
4509
|
+
"""The (family, name, file) exports_public_api counts: each public name
|
|
4510
|
+
but a namespace with members, in its place each member, and so a member
|
|
4511
|
+
that is also a declared name (or another namespace's member) once."""
|
|
4210
4512
|
keys: set[tuple[str, str, str | None]] = set()
|
|
4211
4513
|
for (family, _), info in public.items():
|
|
4212
4514
|
if _has_members(info):
|
|
@@ -4216,6 +4518,16 @@ def _public_keys(public: dict[tuple[str, str], dict]) -> set[tuple[str, str, str
|
|
|
4216
4518
|
return keys
|
|
4217
4519
|
|
|
4218
4520
|
|
|
4521
|
+
def _other_keys(others: list[tuple[str, dict]]) -> set[tuple[str, str, str | None]]:
|
|
4522
|
+
"""The (family, name, file) of the other in-scope definitions of public
|
|
4523
|
+
names (#830), which join the public keys in the set effective_denominator
|
|
4524
|
+
and curated_subset read, never exports_public_api: a name still counts
|
|
4525
|
+
once, when any of its files matches."""
|
|
4526
|
+
return {(family, info["name"], info["file"]) for family, info in others
|
|
4527
|
+
if isinstance(info.get("name"), str) and info["name"] and isinstance(info.get("file"), str)
|
|
4528
|
+
and info["file"]}
|
|
4529
|
+
|
|
4530
|
+
|
|
4219
4531
|
def _head_cap(value: int | None, tier: str | None, scope_type: str | None) -> int | None:
|
|
4220
4532
|
if value is not None:
|
|
4221
4533
|
return value or None
|
|
@@ -4223,7 +4535,10 @@ def _head_cap(value: int | None, tier: str | None, scope_type: str | None) -> in
|
|
|
4223
4535
|
|
|
4224
4536
|
|
|
4225
4537
|
def _workspaces(root: Path, tree: list[str], tree_set: set[str]) -> dict:
|
|
4226
|
-
"""skf-detect-workspaces.py's
|
|
4538
|
+
"""skf-detect-workspaces.py's result over `tree` (its workspaces
|
|
4539
|
+
resolved against those paths), with the content of each root manifest
|
|
4540
|
+
of WORKSPACE_MANIFESTS that `tree_set` lists. No brief scope applies:
|
|
4541
|
+
the monorepo arm reads it through _monorepo."""
|
|
4227
4542
|
manifests = {}
|
|
4228
4543
|
for name in WORKSPACE_MANIFESTS:
|
|
4229
4544
|
if name in tree_set:
|
|
@@ -4233,15 +4548,24 @@ def _workspaces(root: Path, tree: list[str], tree_set: set[str]) -> dict:
|
|
|
4233
4548
|
return _sibling("skf-detect-workspaces.py").detect({"tree": tree, "manifests": manifests})
|
|
4234
4549
|
|
|
4235
4550
|
|
|
4236
|
-
def _monorepo(root: Path, tree: list[str],
|
|
4237
|
-
|
|
4238
|
-
|
|
4551
|
+
def _monorepo(root: Path, tree: list[str], excludes: list[str],
|
|
4552
|
+
glob_match: Callable[[str, str], bool]) -> tuple[bool, str | None]:
|
|
4553
|
+
"""Arm (a): (is the source a monorepo, its manifest kind), from
|
|
4554
|
+
skf-detect-workspaces.py over the tree the brief keeps, its
|
|
4555
|
+
scoped_tree: a workspace member counts only through the files no
|
|
4556
|
+
scope.exclude glob matches, every root manifest stays, and a workspace
|
|
4557
|
+
with no member left is none (#780)."""
|
|
4558
|
+
kept = _sibling("skf-detect-workspaces.py").scoped_tree(tree, excludes, glob_match)
|
|
4559
|
+
result = _workspaces(root, kept, set(kept))
|
|
4239
4560
|
return bool(result.get("is_monorepo")), result.get("manifest_kind")
|
|
4240
4561
|
|
|
4241
4562
|
|
|
4242
4563
|
def _workspace_sources(result: dict, tree: list[str], wanted: set[str]) -> list[set[str]]:
|
|
4243
|
-
"""The source files (of a language --language selects)
|
|
4244
|
-
|
|
4564
|
+
"""The source files (of a language --language selects) under each
|
|
4565
|
+
workspace of `result`, one set per workspace that holds any. _scope_facts
|
|
4566
|
+
passes the workspaces skf-detect-workspaces.py finds over the whole
|
|
4567
|
+
tree, scope.exclude aside (unlike arm (a)): a member the brief drops is
|
|
4568
|
+
still part of the repository the brief chose a subset of."""
|
|
4245
4569
|
sources = []
|
|
4246
4570
|
for workspace in result.get("workspaces") or [] if result.get("is_monorepo") else []:
|
|
4247
4571
|
folder = _norm(workspace.get("path") if isinstance(workspace, dict) else "").strip("/")
|
|
@@ -4288,8 +4612,10 @@ def curated_subset(counted: set[tuple[str, str, str | None]], basis: str, tree:
|
|
|
4288
4612
|
whole workspace (in a single-package repository, the whole package): the
|
|
4289
4613
|
files the basis globs list (a tier A list's, among those scope.include
|
|
4290
4614
|
lists; under arm (b), less those scope.exclude drops) leave out a file
|
|
4291
|
-
that defines a counted public name or
|
|
4292
|
-
|
|
4615
|
+
that defines a counted public name or every source file of one of
|
|
4616
|
+
`workspace_sources` (the workspaces found over the whole tree,
|
|
4617
|
+
scope.exclude aside, so excluding a member the include globs leave out
|
|
4618
|
+
anyway changes nothing). A basis of all-files lists none."""
|
|
4293
4619
|
if basis == "all-files":
|
|
4294
4620
|
return False
|
|
4295
4621
|
files = {f for f in tree if not includes or any(glob_match(f, g) for g in includes)}
|
|
@@ -4413,7 +4739,7 @@ def _scope_facts(args: argparse.Namespace, root: Path) -> dict:
|
|
|
4413
4739
|
|
|
4414
4740
|
# The entry points and the arms need no ast-grep.
|
|
4415
4741
|
in_scope_set = set(in_scope)
|
|
4416
|
-
surfaces = {family: _Surface() for family in FAMILIES}
|
|
4742
|
+
surfaces = {family: _Surface(family, in_scope_set) for family in FAMILIES}
|
|
4417
4743
|
js_entries, maps, unresolved_subpaths = _js_entries(root, in_scope, tree, tree_set, warnings)
|
|
4418
4744
|
py_entries = _py_entries(in_scope, tree_set)
|
|
4419
4745
|
rust_entries = _rust_entries(root, in_scope, tree, tree_set)
|
|
@@ -4428,11 +4754,10 @@ def _scope_facts(args: argparse.Namespace, root: Path) -> dict:
|
|
|
4428
4754
|
f"{entry['package']}) is outside the files in scope: widen scope.include to document "
|
|
4429
4755
|
"the names it exports (entry_point_diff.outside_scope), which effective_denominator "
|
|
4430
4756
|
"leaves out")
|
|
4431
|
-
|
|
4432
|
-
monorepo = bool(workspaces.get("is_monorepo"))
|
|
4757
|
+
monorepo, monorepo_kind = _monorepo(root, tree, excludes, glob_match)
|
|
4433
4758
|
arms = {
|
|
4434
4759
|
"monorepo": monorepo,
|
|
4435
|
-
"monorepo_kind":
|
|
4760
|
+
"monorepo_kind": monorepo_kind,
|
|
4436
4761
|
"specific_modules": scope_type == "specific-modules" and not monorepo,
|
|
4437
4762
|
"multi_subpath_exports": not monorepo and any(m["non_root_subpaths"] > 1 for m in maps),
|
|
4438
4763
|
}
|
|
@@ -4442,7 +4767,8 @@ def _scope_facts(args: argparse.Namespace, root: Path) -> dict:
|
|
|
4442
4767
|
"in_scope": in_scope, "without_recipes": without_recipes, "unmatched_include": unmatched_include,
|
|
4443
4768
|
"unmatched_tier_a": unmatched_tier_a, "surfaces": surfaces, "js_entries": js_entries, "maps": maps,
|
|
4444
4769
|
"unresolved_subpaths": unresolved_subpaths, "py_entries": py_entries, "rust_entries": rust_entries,
|
|
4445
|
-
"entries": entries, "arms": arms,
|
|
4770
|
+
"entries": entries, "arms": arms,
|
|
4771
|
+
"workspace_sources": _workspace_sources(_workspaces(root, tree, tree_set), tree, wanted),
|
|
4446
4772
|
}
|
|
4447
4773
|
|
|
4448
4774
|
|
|
@@ -4586,7 +4912,7 @@ def run_full(args: argparse.Namespace) -> tuple[dict, int]:
|
|
|
4586
4912
|
for record in exports:
|
|
4587
4913
|
if record["from"] and record["language"] in JS_FAMILY:
|
|
4588
4914
|
record["from_file"] = resolve_js_module(record["from"], record["source_file"], tree_set)
|
|
4589
|
-
statuses, public, internal, gaps, outside = entry_point_diff(exports, surfaces, entries, in_scope_set)
|
|
4915
|
+
statuses, public, internal, gaps, outside, others = entry_point_diff(exports, surfaces, entries, in_scope_set)
|
|
4590
4916
|
if "barrel" in statuses.values():
|
|
4591
4917
|
status = "barrel"
|
|
4592
4918
|
else:
|
|
@@ -4596,8 +4922,10 @@ def run_full(args: argparse.Namespace) -> tuple[dict, int]:
|
|
|
4596
4922
|
# only files an exclude drops, that scope.include leaves out or that
|
|
4597
4923
|
# define no public name) narrows nothing, once the run counted a name
|
|
4598
4924
|
# and read every file (no error).
|
|
4599
|
-
counted
|
|
4600
|
-
|
|
4925
|
+
# Each other in-scope definition of a public name counts its name where any of its files is counted (#830).
|
|
4926
|
+
public_keys = _public_keys(public)
|
|
4927
|
+
counted = public_keys | _other_keys(others)
|
|
4928
|
+
counts = _denominator_counts(facts, counted, len(public_keys), len(internal), bool(errors), warnings)
|
|
4601
4929
|
|
|
4602
4930
|
by_type: Counter = Counter(e["export_type"] for e in exports)
|
|
4603
4931
|
ordered = sorted(public.items(), key=lambda item: (item[0][1], item[0][0]))
|
|
@@ -4611,6 +4939,8 @@ def run_full(args: argparse.Namespace) -> tuple[dict, int]:
|
|
|
4611
4939
|
"unresolved": unresolved_subpaths + [u for s in surfaces.values() for u in s.unresolved]},
|
|
4612
4940
|
"entry_point_diff": {
|
|
4613
4941
|
"public": [dict(info, language=family) for (family, _), info in ordered],
|
|
4942
|
+
"other_definitions": [dict(info, language=family) for family, info in
|
|
4943
|
+
sorted(others, key=lambda o: (o[1]["name"], o[1]["file"], o[0]))],
|
|
4614
4944
|
"internal": [{"name": e["export_name"], "language": FAMILY_OF[e["language"]],
|
|
4615
4945
|
"source_file": e["source_file"], "source_line": e["source_line"]} for e in internal],
|
|
4616
4946
|
"extraction_gaps": [{"name": info["name"], "language": family, "entry": info["entry"],
|
|
@@ -4730,6 +5060,8 @@ def run_denominator(args: argparse.Namespace, payload: dict) -> dict:
|
|
|
4730
5060
|
# The exports found, each key at its file: the payload's and the runner's.
|
|
4731
5061
|
found: dict[tuple[str, str], str | None] = {}
|
|
4732
5062
|
internal: set[tuple[str, str]] = set()
|
|
5063
|
+
# The runner's other in-scope definitions of its public names (#830), counted where the names are.
|
|
5064
|
+
others: list[tuple[str, dict]] = []
|
|
4733
5065
|
incomplete = False
|
|
4734
5066
|
runner_status = None
|
|
4735
5067
|
if args.extraction:
|
|
@@ -4744,6 +5076,8 @@ def run_denominator(args: argparse.Namespace, payload: dict) -> dict:
|
|
|
4744
5076
|
key = (str(info.get("language") or ""), info["name"])
|
|
4745
5077
|
public[key] = runner_public[key] = info
|
|
4746
5078
|
found[key] = info.get("file")
|
|
5079
|
+
others = [(str(info.get("language") or ""), info) for info in diff.get("other_definitions") or []
|
|
5080
|
+
if isinstance(info, dict)]
|
|
4747
5081
|
for item in diff.get("internal") or []:
|
|
4748
5082
|
if isinstance(item, dict) and isinstance(item.get("name"), str):
|
|
4749
5083
|
internal.add((str(item.get("language") or ""), item["name"]))
|
|
@@ -4767,11 +5101,27 @@ def run_denominator(args: argparse.Namespace, payload: dict) -> dict:
|
|
|
4767
5101
|
for item in _payload_names(payload.get("exports"), "exports"):
|
|
4768
5102
|
key = (_name_family(item["language"], item["file"]), item["name"])
|
|
4769
5103
|
found.setdefault(key, item["file"])
|
|
4770
|
-
if not item["name"]
|
|
5104
|
+
if not _private(key[0], item["name"]):
|
|
4771
5105
|
internal.add(key)
|
|
4772
5106
|
named = set(public) | {(family, m["name"]) for (family, _), info in public.items() if _has_members(info)
|
|
4773
5107
|
for m in info["members"]}
|
|
4774
|
-
|
|
5108
|
+
public_keys = _public_keys(public)
|
|
5109
|
+
def still_held(key: tuple[str, str]) -> bool:
|
|
5110
|
+
"""Whether the record kept for the runner's public name `key` still names the runner row's file, a
|
|
5111
|
+
namespace with members in neither (full mode keeps no other definition beside one)."""
|
|
5112
|
+
runner_row, kept = runner_public.get(key), public.get(key)
|
|
5113
|
+
if runner_row is None or kept is None or _has_members(runner_row) or _has_members(kept):
|
|
5114
|
+
return False
|
|
5115
|
+
files = [_norm(row["file"]) if isinstance(row.get("file"), str) and row["file"] else None
|
|
5116
|
+
for row in (runner_row, kept)]
|
|
5117
|
+
return files[0] == files[1]
|
|
5118
|
+
|
|
5119
|
+
# A runner row counts only while the record kept for its name names the runner's file: an orphan row, or
|
|
5120
|
+
# one of a name a payload or --quick record moved to another file, adds nothing.
|
|
5121
|
+
others = [(family, info) for family, info in others
|
|
5122
|
+
if isinstance(info.get("name"), str) and still_held((family, info["name"]))]
|
|
5123
|
+
other_keys = _other_keys(others)
|
|
5124
|
+
counted = public_keys | other_keys
|
|
4775
5125
|
|
|
4776
5126
|
if args.source_root:
|
|
4777
5127
|
root = Path(args.source_root)
|
|
@@ -4807,7 +5157,7 @@ def run_denominator(args: argparse.Namespace, payload: dict) -> dict:
|
|
|
4807
5157
|
outside_tree = sorted((name, file) for _, name, file in counted if file and file not in tree_set)
|
|
4808
5158
|
warnings += [f"public name {name} is defined in {file}, which is not in the tree: effective_denominator "
|
|
4809
5159
|
"and curated_subset leave it out" for name, file in outside_tree]
|
|
4810
|
-
counts = _denominator_counts(facts, counted, len(
|
|
5160
|
+
counts = _denominator_counts(facts, counted, len(public_keys), len(internal - named), incomplete, warnings)
|
|
4811
5161
|
if not args.source_root and counts["effective_denominator_decision"]["reason"] not in ("reference-app",
|
|
4812
5162
|
"incomplete-run"):
|
|
4813
5163
|
counts["effective_denominator_decision"] = {"store": False, "reason": "no-source-tree"}
|
|
@@ -4838,7 +5188,9 @@ def run_denominator(args: argparse.Namespace, payload: dict) -> dict:
|
|
|
4838
5188
|
"counts": counts,
|
|
4839
5189
|
"entry_point_diff": {
|
|
4840
5190
|
"public": [row((family, name), file)
|
|
4841
|
-
for family, name, file in sorted(
|
|
5191
|
+
for family, name, file in sorted(public_keys, key=lambda k: (k[1], k[0], k[2] or ""))],
|
|
5192
|
+
"other_definitions": [row((family, name), file)
|
|
5193
|
+
for family, name, file in sorted(other_keys, key=lambda k: (k[1], k[2], k[0]))],
|
|
4842
5194
|
"internal": [row(key, found.get(key)) for key in by_name(internal - named)],
|
|
4843
5195
|
"extraction_gaps": [row(key, given[key]) for key in by_name(given) if key not in found],
|
|
4844
5196
|
},
|
|
@@ -5586,7 +5938,7 @@ def _py_defined_in(root: Path, tree: set[str]) -> Callable[[str, str], str | Non
|
|
|
5586
5938
|
|
|
5587
5939
|
def resolve(file: str, name: str) -> str | None:
|
|
5588
5940
|
if file not in traced:
|
|
5589
|
-
surface = _Surface()
|
|
5941
|
+
surface = _Surface("python")
|
|
5590
5942
|
_py_surface(root, [{"file": file}], tree, surface, [])
|
|
5591
5943
|
traced[file] = {n: info["file"] for n, info in surface.names.items()}
|
|
5592
5944
|
return traced[file].get(name)
|