urtext 0.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/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/analyze/blast-radius.d.ts +28 -0
- package/dist/analyze/blast-radius.js +163 -0
- package/dist/analyze/canonical.d.ts +27 -0
- package/dist/analyze/canonical.js +74 -0
- package/dist/analyze/citations.d.ts +256 -0
- package/dist/analyze/citations.js +945 -0
- package/dist/analyze/effects.d.ts +15 -0
- package/dist/analyze/effects.js +255 -0
- package/dist/analyze/fact.d.ts +42 -0
- package/dist/analyze/fact.js +46 -0
- package/dist/analyze/guards.d.ts +70 -0
- package/dist/analyze/guards.js +211 -0
- package/dist/analyze/index.d.ts +26 -0
- package/dist/analyze/index.js +52 -0
- package/dist/analyze/program.d.ts +15 -0
- package/dist/analyze/program.js +229 -0
- package/dist/analyze/surface.d.ts +48 -0
- package/dist/analyze/surface.js +396 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +12 -0
- package/dist/cli.d.ts +110 -0
- package/dist/cli.js +502 -0
- package/dist/extract/diff.d.ts +35 -0
- package/dist/extract/diff.js +116 -0
- package/dist/extract/git.d.ts +12 -0
- package/dist/extract/git.js +247 -0
- package/dist/extract/index.d.ts +4 -0
- package/dist/extract/index.js +57 -0
- package/dist/extract/intent.d.ts +64 -0
- package/dist/extract/intent.js +238 -0
- package/dist/extract/scope.d.ts +160 -0
- package/dist/extract/scope.js +284 -0
- package/dist/extract/symbols.d.ts +24 -0
- package/dist/extract/symbols.js +230 -0
- package/dist/interpret/client.d.ts +27 -0
- package/dist/interpret/client.js +80 -0
- package/dist/interpret/index.d.ts +41 -0
- package/dist/interpret/index.js +86 -0
- package/dist/interpret/prompt.d.ts +23 -0
- package/dist/interpret/prompt.js +128 -0
- package/dist/interpret/schema.d.ts +74 -0
- package/dist/interpret/schema.js +103 -0
- package/dist/report/conceal.d.ts +63 -0
- package/dist/report/conceal.js +129 -0
- package/dist/report/coverage.d.ts +43 -0
- package/dist/report/coverage.js +56 -0
- package/dist/report/html.d.ts +4 -0
- package/dist/report/html.js +634 -0
- package/dist/report/markdown.d.ts +2 -0
- package/dist/report/markdown.js +168 -0
- package/dist/report/model.d.ts +303 -0
- package/dist/report/model.js +289 -0
- package/dist/report/pdf.d.ts +2 -0
- package/dist/report/pdf.js +217 -0
- package/dist/report/terminal.d.ts +2 -0
- package/dist/report/terminal.js +206 -0
- package/dist/report/write.d.ts +105 -0
- package/dist/report/write.js +160 -0
- package/dist/score/index.d.ts +94 -0
- package/dist/score/index.js +572 -0
- package/dist/score/reach.d.ts +126 -0
- package/dist/score/reach.js +320 -0
- package/dist/score/reconcile.d.ts +52 -0
- package/dist/score/reconcile.js +208 -0
- package/dist/types.d.ts +221 -0
- package/dist/types.js +10 -0
- package/fonts/DejaVuSans-Bold.ttf +0 -0
- package/fonts/DejaVuSans-Oblique.ttf +0 -0
- package/fonts/DejaVuSans.ttf +0 -0
- package/fonts/DejaVuSansMono.ttf +0 -0
- package/fonts/LICENSE +187 -0
- package/package.json +44 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
/**
|
|
3
|
+
* How a name is qualified by the scopes around it. One rule, in one place,
|
|
4
|
+
* because two consumers depend on it agreeing with itself: `mapSymbols`
|
|
5
|
+
* (`./symbols.ts`) groups declarations by qualified name, and `collectGuards`
|
|
6
|
+
* (`../analyze/guards.ts`) attributes a guard to the qualified owner it runs
|
|
7
|
+
* in. `foldReach` then matches facts from different analyzers on that string,
|
|
8
|
+
* so a name qualified two different ways is a fact about one symbol filed
|
|
9
|
+
* under another.
|
|
10
|
+
*
|
|
11
|
+
* "Agreeing with itself" has to mean more than "both call `frameNameOf`". It
|
|
12
|
+
* did not, once: the declaration side grew a rule for unnamed scopes that the
|
|
13
|
+
* guards side never got, and neither framed an object literal, so a guard in
|
|
14
|
+
* `handlers.run` was attributed to a top-level `run` beside it — which stole
|
|
15
|
+
* that export's reference count, and, when the export lost a guard of the same
|
|
16
|
+
* kind in the same change, cancelled it out and reported nothing at all. Both
|
|
17
|
+
* sides now build their frame stack with `framesFor` and mark statement-scope
|
|
18
|
+
* locals with `inStatementScope`, which is where the rule lives;
|
|
19
|
+
* `test/extract/scope.test.ts` asks both of them about the same declaration
|
|
20
|
+
* and compares.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Path for something that belongs to no declaration — code at the top level of
|
|
24
|
+
* a file. Angle brackets because an identifier cannot contain them, so a
|
|
25
|
+
* sentinel cannot collide with a path built out of identifiers. Exported
|
|
26
|
+
* because the renderer has to recognise it to translate it (see
|
|
27
|
+
* `guardOwnerLabel` in `../score/index.ts`).
|
|
28
|
+
*
|
|
29
|
+
* Not collision-proof against every string TypeScript will accept as a frame:
|
|
30
|
+
* `declare module "<module>" { ... }` takes its frame from a *string literal*,
|
|
31
|
+
* and would put this exact text in a path. That is inert here — an ambient
|
|
32
|
+
* module's members are not exports of the file being reviewed, so nothing
|
|
33
|
+
* matches on them — but the guarantee is about identifiers, not about
|
|
34
|
+
* arbitrary module specifiers.
|
|
35
|
+
*/
|
|
36
|
+
export declare const MODULE_OWNER = "<module>";
|
|
37
|
+
/**
|
|
38
|
+
* Segment for a function or object with no name to be known by: an arrow
|
|
39
|
+
* function or expression bound to nothing nameable, a method with a computed
|
|
40
|
+
* key, an object literal passed straight to a call. It can appear at any
|
|
41
|
+
* position in a path —
|
|
42
|
+
* `<anonymous>.inner` for a named function declared inside a callback,
|
|
43
|
+
* `Cls.<anonymous>` for a computed-key method — so anything that renders a path
|
|
44
|
+
* has to handle it anywhere, not only at the end.
|
|
45
|
+
*/
|
|
46
|
+
export declare const ANONYMOUS_OWNER = "<anonymous>";
|
|
47
|
+
/**
|
|
48
|
+
* Segment for a name declared inside a *statement* scope — a bare block, a
|
|
49
|
+
* `for` header or body, a `catch` clause, a `case` block, a class's static
|
|
50
|
+
* initializer block, or a function's body. Such a name is by construction not
|
|
51
|
+
* a member of anything (`export` is legal only at the top level of a file or a
|
|
52
|
+
* namespace), and without this segment it would be qualified as if it were
|
|
53
|
+
* one: at the top of a file it collided with a top-level export of the same
|
|
54
|
+
* name, and inside a named frame it collided with a real member — a
|
|
55
|
+
* static-block local wore the path of the class's static method, and a
|
|
56
|
+
* function-local object inside `function api` wore the path of a merged
|
|
57
|
+
* `namespace api`'s genuinely exported member. The segment applies wherever
|
|
58
|
+
* the declaration sits, not only when the frame stack is empty — the
|
|
59
|
+
* empty-stack-only version was exactly the second collision.
|
|
60
|
+
*
|
|
61
|
+
* Statement scopes still contribute no frame of their own to the *owner*
|
|
62
|
+
* stack: a guard's own body is a block — `if (x) { throw }` inside `validate`
|
|
63
|
+
* belongs to `validate`, not to a scope between them — and framing every
|
|
64
|
+
* block would bury real paths. The marker enters a path only where a name is
|
|
65
|
+
* introduced: once, before the outermost name declared in statement position.
|
|
66
|
+
*/
|
|
67
|
+
export declare const LOCAL_SCOPE = "<local>";
|
|
68
|
+
/**
|
|
69
|
+
* Frame prefixes for accessors: `get value` and `set value` are two distinct
|
|
70
|
+
* runtime symbols — an object can carry either without the other, and a check
|
|
71
|
+
* moved from one to the other genuinely stops running on reads — so they must
|
|
72
|
+
* not share the one path `value` (which is also what a sibling *method* named
|
|
73
|
+
* `value` would wear). The embedded space is what makes the frame
|
|
74
|
+
* collision-free: an identifier cannot contain one, so no method or property
|
|
75
|
+
* name can ever spell `get value`. Like the sentinels, a segment carrying one
|
|
76
|
+
* of these prefixes is not text a reader can find in the source verbatim, so
|
|
77
|
+
* `guardOwnerLabel` in `../score/index.ts` translates it ("the value getter")
|
|
78
|
+
* rather than printing it raw. The same caveat as `MODULE_OWNER` applies:
|
|
79
|
+
* `declare module "get x"` could put this shape in a path, and is inert here
|
|
80
|
+
* for the same reason.
|
|
81
|
+
*/
|
|
82
|
+
export declare const GETTER_FRAME_PREFIX = "get ";
|
|
83
|
+
export declare const SETTER_FRAME_PREFIX = "set ";
|
|
84
|
+
/**
|
|
85
|
+
* Every sentinel a qualified path can contain. Anything that renders a path to
|
|
86
|
+
* a reader has to handle all of them, at any position — `guardOwnerLabel` in
|
|
87
|
+
* `../score/index.ts` does, and `test/score/index.test.ts`, "translates every
|
|
88
|
+
* scope sentinel wherever it sits in the path", walks this list rather than a
|
|
89
|
+
* remembered copy of it, so adding a fourth sentinel here fails that test until
|
|
90
|
+
* it has a translation.
|
|
91
|
+
*/
|
|
92
|
+
export declare const SCOPE_SENTINELS: readonly string[];
|
|
93
|
+
/**
|
|
94
|
+
* The statically known text of a member name: an identifier, or a private
|
|
95
|
+
* `#name` — which is spelled in full at its declaration and cannot collide
|
|
96
|
+
* with any identifier, since `#` is not an identifier character. Throwing a
|
|
97
|
+
* `#name` away (the old Identifier-only check) framed every private method
|
|
98
|
+
* `<anonymous>`, where it shared a path with any computed-key sibling and a
|
|
99
|
+
* guard moved between the two cancelled to silence. Computed keys and
|
|
100
|
+
* string or numeric names stay undefined: only a computed key's *expression*
|
|
101
|
+
* is known statically, not the name it evaluates to.
|
|
102
|
+
*/
|
|
103
|
+
export declare function memberNameOf(name: ts.PropertyName): string | undefined;
|
|
104
|
+
/**
|
|
105
|
+
* The frame this node contributes to the qualified names inside it, or
|
|
106
|
+
* undefined for a node that introduces none.
|
|
107
|
+
*
|
|
108
|
+
* Every function-like node contributes one, falling back to `ANONYMOUS_OWNER`
|
|
109
|
+
* rather than to nothing: a node that opens a scope and contributes no frame
|
|
110
|
+
* would let its contents be qualified as if they sat in the scope outside it.
|
|
111
|
+
* Classes, namespaces, and object literals contribute one so that a method
|
|
112
|
+
* comes out as `Worker.run`, `N.run`, or `handlers.run`. `export default class
|
|
113
|
+
* {}` and `export default function () {}` both use "default", the name they
|
|
114
|
+
* are exported under — a nameless class or function declaration is legal
|
|
115
|
+
* nowhere else, and no identifier can collide with `default` since it is a
|
|
116
|
+
* reserved word.
|
|
117
|
+
*
|
|
118
|
+
* An expression's *internal* name is never used, even when it has one:
|
|
119
|
+
* `const format = function format2() {}` frames `format`, not `format2`.
|
|
120
|
+
* `format2` is in scope only inside the expression, so a path built from it
|
|
121
|
+
* names something no other file can reach — and would collide with a real
|
|
122
|
+
* top-level `format2` declared beside it. The binding is what the rest of the
|
|
123
|
+
* program calls this function, and it is what `mapSymbols` records.
|
|
124
|
+
*
|
|
125
|
+
* Two things this does not frame, deliberately. A class body holds no
|
|
126
|
+
* statements, so a class frame is normally only a prefix for its members —
|
|
127
|
+
* except for a static initializer block, which does hold statements and
|
|
128
|
+
* introduces no frame of its own, so a guard inside one is attributed to the
|
|
129
|
+
* class (`test/analyze/guards.test.ts`, "attributes a static initializer
|
|
130
|
+
* block's guard to the class"). And a plain statement block is not a frame at
|
|
131
|
+
* all — see `LOCAL_SCOPE`.
|
|
132
|
+
*/
|
|
133
|
+
export declare function frameNameOf(node: ts.Node): string | undefined;
|
|
134
|
+
/**
|
|
135
|
+
* The segments this node pushes onto a frame stack: none, its frame, or —
|
|
136
|
+
* when the node itself is declared in statement position — `LOCAL_SCOPE` and
|
|
137
|
+
* its frame. Both walkers push through here, which is what makes the local
|
|
138
|
+
* rule one rule rather than two that have to be kept in step.
|
|
139
|
+
*/
|
|
140
|
+
export declare function framesFor(node: ts.Node): string[];
|
|
141
|
+
/**
|
|
142
|
+
* A frame stack as one dotted path, or `MODULE_OWNER` for an empty stack —
|
|
143
|
+
* top-level code, which belongs to no declaration. The stack already carries
|
|
144
|
+
* its root marker when it needs one (see `framesFor`), so this is a join and
|
|
145
|
+
* not a second place the rule is decided.
|
|
146
|
+
*/
|
|
147
|
+
export declare function qualifyOwner(frames: readonly string[]): string;
|
|
148
|
+
/**
|
|
149
|
+
* A declared name qualified by the frames around it. Unlike `qualifyOwner`
|
|
150
|
+
* there is always a name to fall back on, so an empty stack is not a sentinel —
|
|
151
|
+
* a top-level declaration's qualified name is simply its name, which is what
|
|
152
|
+
* makes `qualifiedName` equal to `name` for everything a module exports.
|
|
153
|
+
*
|
|
154
|
+
* The local rule reappears here for one reason: a declaration is recorded
|
|
155
|
+
* without any frame having been pushed for it, so its own position in a
|
|
156
|
+
* statement scope has to be asked about here — a bare `const` in a top-level
|
|
157
|
+
* block, but equally one in a static block or a function body, where the
|
|
158
|
+
* frame stack is not empty. Same predicate as `framesFor`, so the two agree.
|
|
159
|
+
*/
|
|
160
|
+
export declare function qualifyDeclaration(frames: readonly string[], name: string, node: ts.Node): string;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
/**
|
|
3
|
+
* How a name is qualified by the scopes around it. One rule, in one place,
|
|
4
|
+
* because two consumers depend on it agreeing with itself: `mapSymbols`
|
|
5
|
+
* (`./symbols.ts`) groups declarations by qualified name, and `collectGuards`
|
|
6
|
+
* (`../analyze/guards.ts`) attributes a guard to the qualified owner it runs
|
|
7
|
+
* in. `foldReach` then matches facts from different analyzers on that string,
|
|
8
|
+
* so a name qualified two different ways is a fact about one symbol filed
|
|
9
|
+
* under another.
|
|
10
|
+
*
|
|
11
|
+
* "Agreeing with itself" has to mean more than "both call `frameNameOf`". It
|
|
12
|
+
* did not, once: the declaration side grew a rule for unnamed scopes that the
|
|
13
|
+
* guards side never got, and neither framed an object literal, so a guard in
|
|
14
|
+
* `handlers.run` was attributed to a top-level `run` beside it — which stole
|
|
15
|
+
* that export's reference count, and, when the export lost a guard of the same
|
|
16
|
+
* kind in the same change, cancelled it out and reported nothing at all. Both
|
|
17
|
+
* sides now build their frame stack with `framesFor` and mark statement-scope
|
|
18
|
+
* locals with `inStatementScope`, which is where the rule lives;
|
|
19
|
+
* `test/extract/scope.test.ts` asks both of them about the same declaration
|
|
20
|
+
* and compares.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Path for something that belongs to no declaration — code at the top level of
|
|
24
|
+
* a file. Angle brackets because an identifier cannot contain them, so a
|
|
25
|
+
* sentinel cannot collide with a path built out of identifiers. Exported
|
|
26
|
+
* because the renderer has to recognise it to translate it (see
|
|
27
|
+
* `guardOwnerLabel` in `../score/index.ts`).
|
|
28
|
+
*
|
|
29
|
+
* Not collision-proof against every string TypeScript will accept as a frame:
|
|
30
|
+
* `declare module "<module>" { ... }` takes its frame from a *string literal*,
|
|
31
|
+
* and would put this exact text in a path. That is inert here — an ambient
|
|
32
|
+
* module's members are not exports of the file being reviewed, so nothing
|
|
33
|
+
* matches on them — but the guarantee is about identifiers, not about
|
|
34
|
+
* arbitrary module specifiers.
|
|
35
|
+
*/
|
|
36
|
+
export const MODULE_OWNER = "<module>";
|
|
37
|
+
/**
|
|
38
|
+
* Segment for a function or object with no name to be known by: an arrow
|
|
39
|
+
* function or expression bound to nothing nameable, a method with a computed
|
|
40
|
+
* key, an object literal passed straight to a call. It can appear at any
|
|
41
|
+
* position in a path —
|
|
42
|
+
* `<anonymous>.inner` for a named function declared inside a callback,
|
|
43
|
+
* `Cls.<anonymous>` for a computed-key method — so anything that renders a path
|
|
44
|
+
* has to handle it anywhere, not only at the end.
|
|
45
|
+
*/
|
|
46
|
+
export const ANONYMOUS_OWNER = "<anonymous>";
|
|
47
|
+
/**
|
|
48
|
+
* Segment for a name declared inside a *statement* scope — a bare block, a
|
|
49
|
+
* `for` header or body, a `catch` clause, a `case` block, a class's static
|
|
50
|
+
* initializer block, or a function's body. Such a name is by construction not
|
|
51
|
+
* a member of anything (`export` is legal only at the top level of a file or a
|
|
52
|
+
* namespace), and without this segment it would be qualified as if it were
|
|
53
|
+
* one: at the top of a file it collided with a top-level export of the same
|
|
54
|
+
* name, and inside a named frame it collided with a real member — a
|
|
55
|
+
* static-block local wore the path of the class's static method, and a
|
|
56
|
+
* function-local object inside `function api` wore the path of a merged
|
|
57
|
+
* `namespace api`'s genuinely exported member. The segment applies wherever
|
|
58
|
+
* the declaration sits, not only when the frame stack is empty — the
|
|
59
|
+
* empty-stack-only version was exactly the second collision.
|
|
60
|
+
*
|
|
61
|
+
* Statement scopes still contribute no frame of their own to the *owner*
|
|
62
|
+
* stack: a guard's own body is a block — `if (x) { throw }` inside `validate`
|
|
63
|
+
* belongs to `validate`, not to a scope between them — and framing every
|
|
64
|
+
* block would bury real paths. The marker enters a path only where a name is
|
|
65
|
+
* introduced: once, before the outermost name declared in statement position.
|
|
66
|
+
*/
|
|
67
|
+
export const LOCAL_SCOPE = "<local>";
|
|
68
|
+
/**
|
|
69
|
+
* Frame prefixes for accessors: `get value` and `set value` are two distinct
|
|
70
|
+
* runtime symbols — an object can carry either without the other, and a check
|
|
71
|
+
* moved from one to the other genuinely stops running on reads — so they must
|
|
72
|
+
* not share the one path `value` (which is also what a sibling *method* named
|
|
73
|
+
* `value` would wear). The embedded space is what makes the frame
|
|
74
|
+
* collision-free: an identifier cannot contain one, so no method or property
|
|
75
|
+
* name can ever spell `get value`. Like the sentinels, a segment carrying one
|
|
76
|
+
* of these prefixes is not text a reader can find in the source verbatim, so
|
|
77
|
+
* `guardOwnerLabel` in `../score/index.ts` translates it ("the value getter")
|
|
78
|
+
* rather than printing it raw. The same caveat as `MODULE_OWNER` applies:
|
|
79
|
+
* `declare module "get x"` could put this shape in a path, and is inert here
|
|
80
|
+
* for the same reason.
|
|
81
|
+
*/
|
|
82
|
+
export const GETTER_FRAME_PREFIX = "get ";
|
|
83
|
+
export const SETTER_FRAME_PREFIX = "set ";
|
|
84
|
+
/**
|
|
85
|
+
* Every sentinel a qualified path can contain. Anything that renders a path to
|
|
86
|
+
* a reader has to handle all of them, at any position — `guardOwnerLabel` in
|
|
87
|
+
* `../score/index.ts` does, and `test/score/index.test.ts`, "translates every
|
|
88
|
+
* scope sentinel wherever it sits in the path", walks this list rather than a
|
|
89
|
+
* remembered copy of it, so adding a fourth sentinel here fails that test until
|
|
90
|
+
* it has a translation.
|
|
91
|
+
*/
|
|
92
|
+
export const SCOPE_SENTINELS = [
|
|
93
|
+
MODULE_OWNER,
|
|
94
|
+
ANONYMOUS_OWNER,
|
|
95
|
+
LOCAL_SCOPE,
|
|
96
|
+
];
|
|
97
|
+
/**
|
|
98
|
+
* The statically known text of a member name: an identifier, or a private
|
|
99
|
+
* `#name` — which is spelled in full at its declaration and cannot collide
|
|
100
|
+
* with any identifier, since `#` is not an identifier character. Throwing a
|
|
101
|
+
* `#name` away (the old Identifier-only check) framed every private method
|
|
102
|
+
* `<anonymous>`, where it shared a path with any computed-key sibling and a
|
|
103
|
+
* guard moved between the two cancelled to silence. Computed keys and
|
|
104
|
+
* string or numeric names stay undefined: only a computed key's *expression*
|
|
105
|
+
* is known statically, not the name it evaluates to.
|
|
106
|
+
*/
|
|
107
|
+
export function memberNameOf(name) {
|
|
108
|
+
if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name))
|
|
109
|
+
return name.text;
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
/** The name an arrow, function expression, class expression, or object literal is bound to. */
|
|
113
|
+
function boundName(node) {
|
|
114
|
+
const parent = node.parent;
|
|
115
|
+
if (!parent)
|
|
116
|
+
return undefined;
|
|
117
|
+
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
|
118
|
+
return parent.name.text;
|
|
119
|
+
}
|
|
120
|
+
if ((ts.isPropertyAssignment(parent) || ts.isPropertyDeclaration(parent)) &&
|
|
121
|
+
memberNameOf(parent.name) !== undefined) {
|
|
122
|
+
return memberNameOf(parent.name);
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The frame this node contributes to the qualified names inside it, or
|
|
128
|
+
* undefined for a node that introduces none.
|
|
129
|
+
*
|
|
130
|
+
* Every function-like node contributes one, falling back to `ANONYMOUS_OWNER`
|
|
131
|
+
* rather than to nothing: a node that opens a scope and contributes no frame
|
|
132
|
+
* would let its contents be qualified as if they sat in the scope outside it.
|
|
133
|
+
* Classes, namespaces, and object literals contribute one so that a method
|
|
134
|
+
* comes out as `Worker.run`, `N.run`, or `handlers.run`. `export default class
|
|
135
|
+
* {}` and `export default function () {}` both use "default", the name they
|
|
136
|
+
* are exported under — a nameless class or function declaration is legal
|
|
137
|
+
* nowhere else, and no identifier can collide with `default` since it is a
|
|
138
|
+
* reserved word.
|
|
139
|
+
*
|
|
140
|
+
* An expression's *internal* name is never used, even when it has one:
|
|
141
|
+
* `const format = function format2() {}` frames `format`, not `format2`.
|
|
142
|
+
* `format2` is in scope only inside the expression, so a path built from it
|
|
143
|
+
* names something no other file can reach — and would collide with a real
|
|
144
|
+
* top-level `format2` declared beside it. The binding is what the rest of the
|
|
145
|
+
* program calls this function, and it is what `mapSymbols` records.
|
|
146
|
+
*
|
|
147
|
+
* Two things this does not frame, deliberately. A class body holds no
|
|
148
|
+
* statements, so a class frame is normally only a prefix for its members —
|
|
149
|
+
* except for a static initializer block, which does hold statements and
|
|
150
|
+
* introduces no frame of its own, so a guard inside one is attributed to the
|
|
151
|
+
* class (`test/analyze/guards.test.ts`, "attributes a static initializer
|
|
152
|
+
* block's guard to the class"). And a plain statement block is not a frame at
|
|
153
|
+
* all — see `LOCAL_SCOPE`.
|
|
154
|
+
*/
|
|
155
|
+
export function frameNameOf(node) {
|
|
156
|
+
if (ts.isClassDeclaration(node)) {
|
|
157
|
+
return node.name?.text ?? "default";
|
|
158
|
+
}
|
|
159
|
+
if (ts.isClassExpression(node) || ts.isObjectLiteralExpression(node)) {
|
|
160
|
+
return boundName(node) ?? ANONYMOUS_OWNER;
|
|
161
|
+
}
|
|
162
|
+
if (ts.isModuleDeclaration(node)) {
|
|
163
|
+
// `namespace N {}` and `declare module "spec" {}` — an Identifier or a
|
|
164
|
+
// StringLiteral, both of which carry `.text`.
|
|
165
|
+
return node.name.text;
|
|
166
|
+
}
|
|
167
|
+
if (ts.isFunctionDeclaration(node)) {
|
|
168
|
+
return node.name?.text ?? "default";
|
|
169
|
+
}
|
|
170
|
+
if (ts.isMethodDeclaration(node)) {
|
|
171
|
+
return memberNameOf(node.name) ?? ANONYMOUS_OWNER;
|
|
172
|
+
}
|
|
173
|
+
if (ts.isConstructorDeclaration(node)) {
|
|
174
|
+
return "constructor";
|
|
175
|
+
}
|
|
176
|
+
if (ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) {
|
|
177
|
+
const name = memberNameOf(node.name);
|
|
178
|
+
if (name === undefined)
|
|
179
|
+
return ANONYMOUS_OWNER;
|
|
180
|
+
// Not the bare name: a getter and a setter are different runtime symbols
|
|
181
|
+
// and must not share a path — see GETTER_FRAME_PREFIX.
|
|
182
|
+
return ts.isGetAccessorDeclaration(node)
|
|
183
|
+
? GETTER_FRAME_PREFIX + name
|
|
184
|
+
: SETTER_FRAME_PREFIX + name;
|
|
185
|
+
}
|
|
186
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
187
|
+
return boundName(node) ?? ANONYMOUS_OWNER;
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* A node that opens a statement scope — the shapes `LOCAL_SCOPE` exists for.
|
|
193
|
+
* Anything carrying a name is already a frame, so this list covers only
|
|
194
|
+
* *statement* scopes. Deliberately not `ts.isModuleBlock`: a namespace's body
|
|
195
|
+
* is where its *members* live — `export` is legal there — so a declaration
|
|
196
|
+
* directly under one is reachable as `N.x` and must not be marked local.
|
|
197
|
+
* `inStatementScope` gives it its own early exit instead.
|
|
198
|
+
*
|
|
199
|
+
* The list has to be complete, and completeness is the whole of its safety:
|
|
200
|
+
* `inStatementScope` walks through any node not listed here, so an unlisted
|
|
201
|
+
* scope qualifies a name declared inside it as if it were a member of the
|
|
202
|
+
* enclosing frame or file — where it can collide with a real declaration of
|
|
203
|
+
* the same name and take that declaration's guard findings and reference
|
|
204
|
+
* count. An earlier version of this comment claimed an omission "can only
|
|
205
|
+
* leave a path shorter", which is not what an omission does; a `for` header
|
|
206
|
+
* was the counterexample, and it was in that state (`for (const f = …)`
|
|
207
|
+
* beside `export const f`, reported as a guard removed from the export, and
|
|
208
|
+
* silent in the other direction). So: every construct that can declare a
|
|
209
|
+
* binding and is not itself a frame belongs here. The three `for` forms are
|
|
210
|
+
* listed for their *headers* — their bodies are already covered when braced,
|
|
211
|
+
* and a braceless body cannot declare anything.
|
|
212
|
+
*/
|
|
213
|
+
function isStatementScope(node) {
|
|
214
|
+
return (ts.isBlock(node) ||
|
|
215
|
+
ts.isCaseBlock(node) ||
|
|
216
|
+
ts.isCatchClause(node) ||
|
|
217
|
+
ts.isClassStaticBlockDeclaration(node) ||
|
|
218
|
+
ts.isForStatement(node) ||
|
|
219
|
+
ts.isForInStatement(node) ||
|
|
220
|
+
ts.isForOfStatement(node));
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Whether this node's declaration site sits inside a statement scope, rather
|
|
224
|
+
* than directly under the file, a namespace body, or a frame. This is the one
|
|
225
|
+
* predicate behind the `LOCAL_SCOPE` segment, and it is a question about the
|
|
226
|
+
* node's *own* position: the walk stops at the nearest enclosing scope-opener
|
|
227
|
+
* of any kind, so a declaration directly under a frame (a class member, an
|
|
228
|
+
* object-literal method, a namespace member) is not local, while the same
|
|
229
|
+
* declaration one statement scope deeper is — whatever sits above that.
|
|
230
|
+
*
|
|
231
|
+
* A function's body block is a statement scope like any other: a `const`
|
|
232
|
+
* declared in it is a local no other file can reach, not a member of the
|
|
233
|
+
* function. Walking through it to the function — the old behaviour, when the
|
|
234
|
+
* root rule fired only on an empty frame stack — is what let a function-local
|
|
235
|
+
* `handlers` wear a merged namespace's member path.
|
|
236
|
+
*/
|
|
237
|
+
function inStatementScope(node) {
|
|
238
|
+
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
239
|
+
if (ts.isSourceFile(ancestor) || ts.isModuleBlock(ancestor))
|
|
240
|
+
return false;
|
|
241
|
+
if (isStatementScope(ancestor))
|
|
242
|
+
return true;
|
|
243
|
+
if (frameNameOf(ancestor) !== undefined)
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The segments this node pushes onto a frame stack: none, its frame, or —
|
|
250
|
+
* when the node itself is declared in statement position — `LOCAL_SCOPE` and
|
|
251
|
+
* its frame. Both walkers push through here, which is what makes the local
|
|
252
|
+
* rule one rule rather than two that have to be kept in step.
|
|
253
|
+
*/
|
|
254
|
+
export function framesFor(node) {
|
|
255
|
+
const frame = frameNameOf(node);
|
|
256
|
+
if (frame === undefined)
|
|
257
|
+
return [];
|
|
258
|
+
return inStatementScope(node) ? [LOCAL_SCOPE, frame] : [frame];
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* A frame stack as one dotted path, or `MODULE_OWNER` for an empty stack —
|
|
262
|
+
* top-level code, which belongs to no declaration. The stack already carries
|
|
263
|
+
* its root marker when it needs one (see `framesFor`), so this is a join and
|
|
264
|
+
* not a second place the rule is decided.
|
|
265
|
+
*/
|
|
266
|
+
export function qualifyOwner(frames) {
|
|
267
|
+
return frames.length === 0 ? MODULE_OWNER : frames.join(".");
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* A declared name qualified by the frames around it. Unlike `qualifyOwner`
|
|
271
|
+
* there is always a name to fall back on, so an empty stack is not a sentinel —
|
|
272
|
+
* a top-level declaration's qualified name is simply its name, which is what
|
|
273
|
+
* makes `qualifiedName` equal to `name` for everything a module exports.
|
|
274
|
+
*
|
|
275
|
+
* The local rule reappears here for one reason: a declaration is recorded
|
|
276
|
+
* without any frame having been pushed for it, so its own position in a
|
|
277
|
+
* statement scope has to be asked about here — a bare `const` in a top-level
|
|
278
|
+
* block, but equally one in a static block or a function body, where the
|
|
279
|
+
* frame stack is not empty. Same predicate as `framesFor`, so the two agree.
|
|
280
|
+
*/
|
|
281
|
+
export function qualifyDeclaration(frames, name, node) {
|
|
282
|
+
const local = inStatementScope(node);
|
|
283
|
+
return [...frames, ...(local ? [LOCAL_SCOPE] : []), name].join(".");
|
|
284
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ChangedSymbol, Hunk } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* A TypeScript *implementation* file, in any of the four extensions the
|
|
4
|
+
* language has — `.ts`, `.tsx`, and the module-explicit `.mts`/`.cts` (there
|
|
5
|
+
* is no `.mtsx`/`.ctsx`; JSX never got module-explicit flavours). Declaration
|
|
6
|
+
* files are excluded in every flavour: `.d.ts`, `.d.mts`, `.d.cts`. The
|
|
7
|
+
* `.tsx?`-only version of this test made every `.mts`/`.cts` file invisible
|
|
8
|
+
* to every analyzer, silently — the worst outcome this tool has.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isTypeScriptFile(path: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Symbols affected by this change — one entry per symbol, not per declaration.
|
|
13
|
+
* A symbol is reported when it is new, gone, or when a hunk falls inside any
|
|
14
|
+
* of its declarations' line ranges in the after-file.
|
|
15
|
+
*
|
|
16
|
+
* One entry per symbol is what the rest of the pipeline is built on:
|
|
17
|
+
* `blast-radius` derives a fact id from `qualifiedName` alone, so N entries
|
|
18
|
+
* for one overloaded export meant N facts sharing an id — N identical
|
|
19
|
+
* `verified` findings, N identical rows in the report's API-surface table,
|
|
20
|
+
* and a single model claim citing that id attaching to every one of them.
|
|
21
|
+
* See `test/identity.test.ts`, "one changed symbol is one symbol, however
|
|
22
|
+
* many declarations it has".
|
|
23
|
+
*/
|
|
24
|
+
export declare function mapSymbols(path: string, before: string | null, after: string | null, hunks: Hunk[]): ChangedSymbol[];
|