ts-axi 0.1.6 → 0.1.7
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/README.md +1 -1
- package/dist/src/commands/impact.js +36 -12
- package/dist/src/commands/refs.js +5 -4
- package/dist/src/symbols.js +87 -18
- package/dist/src/templates.js +43 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -175,7 +175,7 @@ decls[4]{line,kind,name,exported,signature}:
|
|
|
175
175
|
17,function,cartBadge,true,directive cartBadge()
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
-
An edit inside `CartCtrl` is reported by `impact` as `CartCtrl body`, not as a file-level change. Names registered by a string have no identifier the checker can follow; `refs` says so and offers `--grep`. ui-router and ngRoute states are Declarations of kind `state` (`states.cart`, with callbacks as `states.cart.resolve.items`), so an edit inside a `resolve` names that callback. Directives, components, their `scope`/`bindings` keys, controllers and filters also get `templates: <n> usages in <m> files` (kebab-case elements and attributes, `ng-controller`, `| filter` in `.html`/`.htm`/`.ejs`/`.hbs`/`.njk`/`.pug`), a `templates` column in `impact`, and a changed template that uses a changed directive or binding is listed under `template_changes` instead of `skipped`.
|
|
178
|
+
An edit inside `CartCtrl` is reported by `impact` as `CartCtrl body`, not as a file-level change. Names registered by a string have no identifier the checker can follow; `refs` says so and offers `--grep`. ui-router and ngRoute states are Declarations of kind `state` (`states.cart`, with callbacks as `states.cart.resolve.items`), so an edit inside a `resolve` names that callback; state objects in exported arrays (`` module.exports = [{ name: `${base}.init`, url, resolve }] ``) count too, their template-literal names filled from same-file consts. Directives, components, their `scope`/`bindings` keys, controllers and filters also get `templates: <n> usages in <m> files` (kebab-case elements and attributes, `ng-controller`, `| filter` in `.html`/`.htm`/`.ejs`/`.hbs`/`.njk`/`.pug`), a `templates` column in `impact`, and a changed template that uses a changed directive or binding is listed under `template_changes` instead of `skipped`.
|
|
179
179
|
|
|
180
180
|
## Monorepos
|
|
181
181
|
|
|
@@ -29,7 +29,7 @@ flags[7]:
|
|
|
29
29
|
output:
|
|
30
30
|
base, head, changes (count), changed[n]{symbol,kind,change,exported,at,refs_total,refs_outside,via,files,tests}
|
|
31
31
|
change = signature | removed | type | added | body | modified (before unavailable) - riskiest first
|
|
32
|
-
textual = column added only when an untyped (JavaScript; implicit project) Symbol has more whole-word matches outside the change set than refs_outside - those are uses the checker cannot resolve; - for typed rows; such a Change is never local_only
|
|
32
|
+
textual = column added only when an untyped (JavaScript; implicit project) Symbol has more whole-word matches outside the change set than refs_outside - those are uses the checker cannot resolve (declaring file only for a non-exported local); - for typed rows; such a Change is never local_only
|
|
33
33
|
refs_* / files / tests count direct References only; via = References outside the change set that reach the Symbol through an interface or base member it implements (or name an override of it) - they MAY call it
|
|
34
34
|
refs[n]{symbol,file,line,context} = References outside the change set; truncated + refs_shown + refs_outside_total when cut
|
|
35
35
|
notes[n]{symbol,note} = a signature change of a member that implements an interface or base member (the contract may break)
|
|
@@ -231,8 +231,11 @@ function blindSpots(semantic, names, changeSet) {
|
|
|
231
231
|
function walkExtra(dir, outFiles) {
|
|
232
232
|
outFiles.push(...walkFiles(dir, { accept: (name) => GREP_EXTRA.some((ext) => name.endsWith(ext)) }).files);
|
|
233
233
|
}
|
|
234
|
-
/**
|
|
235
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Whole-word matches of `names` in Project files and templates that are not `known` References;
|
|
236
|
+
* a name in `localTo` (name → absolute file) is searched in that file only.
|
|
237
|
+
*/
|
|
238
|
+
export function textualMatches(opened, semantic, names, known, skipLines, changeSet, localTo = new Map()) {
|
|
236
239
|
const files = [...semantic.projectFiles];
|
|
237
240
|
const projectSet = new Set(files);
|
|
238
241
|
walkExtra(opened.root, files);
|
|
@@ -246,6 +249,8 @@ export function textualMatches(opened, semantic, names, known, skipLines, change
|
|
|
246
249
|
const declared = new Set(projectSet.has(abs) ? opened.index.declarations(abs).map((d) => d.nameLine) : []);
|
|
247
250
|
const lines = new Set();
|
|
248
251
|
for (const name of names) {
|
|
252
|
+
if (localTo.has(name) && localTo.get(name) !== abs)
|
|
253
|
+
continue;
|
|
249
254
|
for (const m of wordMatches(text, name)) {
|
|
250
255
|
if (known.has(`${rel}:${m.line}`) || skipLines.has(`${rel}:${m.line}`) || declared.has(m.line) || inRanges(changeSet.get(rel), m.line))
|
|
251
256
|
continue;
|
|
@@ -267,11 +272,18 @@ export function isUntyped(opened, decl, file) {
|
|
|
267
272
|
return false;
|
|
268
273
|
return decl?.nameless === true || opened.index.project.implicit || (file !== undefined && /\.[mc]?jsx?$/.test(file));
|
|
269
274
|
}
|
|
270
|
-
/**
|
|
271
|
-
export function
|
|
275
|
+
/** A non-exported, non-member, identifier-named Declaration: only its own file can name it. */
|
|
276
|
+
export function isPlainLocal(decl) {
|
|
277
|
+
return decl !== undefined && !decl.exported && !decl.nameless && decl.parentName === undefined;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Whole-word occurrences of `name` in Project files outside `changeSet`; lines declaring a `name` excluded.
|
|
281
|
+
* With `onlyFile` (absolute), that file alone.
|
|
282
|
+
*/
|
|
283
|
+
export function textualCount(opened, name, changeSet = new Map(), onlyFile) {
|
|
272
284
|
let matches = 0;
|
|
273
285
|
let files = 0;
|
|
274
|
-
for (const abs of opened.index.files) {
|
|
286
|
+
for (const abs of onlyFile !== undefined ? [onlyFile] : opened.index.files) {
|
|
275
287
|
const text = opened.index.text(abs);
|
|
276
288
|
if (text === undefined || !text.includes(name))
|
|
277
289
|
continue;
|
|
@@ -282,10 +294,10 @@ export function textualCount(opened, name, changeSet = new Map()) {
|
|
|
282
294
|
if (n > 0)
|
|
283
295
|
files++;
|
|
284
296
|
}
|
|
285
|
-
return { matches, files };
|
|
297
|
+
return { matches, files, ...(onlyFile !== undefined ? { fileScoped: true } : {}) };
|
|
286
298
|
}
|
|
287
299
|
export function textualLine(count) {
|
|
288
|
-
return `${count.matches} matches in ${count.files} files`;
|
|
300
|
+
return count.fileScoped ? `${count.matches} matches in this file` : `${count.matches} matches in ${count.files} files`;
|
|
289
301
|
}
|
|
290
302
|
export function textualHint(command, gap) {
|
|
291
303
|
return `Run \`${command}\` for the ${gap} textual matches the checker cannot resolve (untyped JavaScript)`;
|
|
@@ -529,9 +541,10 @@ export async function impactCommand(args, ctx) {
|
|
|
529
541
|
for (const row of rows) {
|
|
530
542
|
if (!row.live || !isUntyped(opened, row.live, row.live.file))
|
|
531
543
|
continue;
|
|
532
|
-
row.
|
|
544
|
+
const localTo = isPlainLocal(row.live) ? new Map([[row.live.name, opened.index.abs(row.live.file)]]) : undefined;
|
|
545
|
+
row.textual = textualCount(opened, row.live.name, changeSet, localTo?.get(row.live.name));
|
|
533
546
|
if (row.textual.matches > 0)
|
|
534
|
-
row.unresolved = textualMatches(opened, semantic, [row.live.name], known, new Set(), changeSet).length;
|
|
547
|
+
row.unresolved = textualMatches(opened, semantic, [row.live.name], known, new Set(), changeSet, localTo).length;
|
|
535
548
|
}
|
|
536
549
|
// Template usages: directives, components, bindings, controllers and filters named in .html and friends.
|
|
537
550
|
let templateFiles;
|
|
@@ -585,14 +598,25 @@ export async function impactCommand(args, ctx) {
|
|
|
585
598
|
};
|
|
586
599
|
// --- Blind spots, textual matches, deeper callers ---
|
|
587
600
|
const names = new Map();
|
|
601
|
+
// A name is file-scoped only when every changed Declaration carrying it is a plain local of one file.
|
|
602
|
+
const localTo = new Map();
|
|
603
|
+
const wide = new Set();
|
|
588
604
|
for (const r of rows) {
|
|
589
|
-
const
|
|
605
|
+
const decl = r.change.after ?? r.change.before;
|
|
606
|
+
const name = decl.name;
|
|
590
607
|
if (!names.has(name))
|
|
591
608
|
names.set(name, r.symbol);
|
|
609
|
+
const abs = opened.index.abs(decl.file);
|
|
610
|
+
if (!isPlainLocal(decl) || (localTo.has(name) && localTo.get(name) !== abs))
|
|
611
|
+
wide.add(name);
|
|
612
|
+
else
|
|
613
|
+
localTo.set(name, abs);
|
|
592
614
|
}
|
|
615
|
+
for (const name of wide)
|
|
616
|
+
localTo.delete(name);
|
|
593
617
|
const spots = blindSpots(semantic, names, changeSet);
|
|
594
618
|
const textRows = grep
|
|
595
|
-
? textualMatches(opened, semantic, [...names.keys()], known, new Set(spots.map((s) => `${s.file}:${s.line}`)), changeSet)
|
|
619
|
+
? textualMatches(opened, semantic, [...names.keys()], known, new Set(spots.map((s) => `${s.file}:${s.line}`)), changeSet, localTo)
|
|
596
620
|
: [];
|
|
597
621
|
const unresolved = grep ? withTemplateRows(textRows, rows, templateFiles ?? []) : [];
|
|
598
622
|
const touched = [...new Set([...analysed.filter((f) => f.status !== "deleted").map((f) => f.path), ...listed.flatMap((r) => r.outside.map((e) => e.file))])];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { relative, sep } from "node:path";
|
|
2
2
|
import { bool, expectPositionals, flag, intFlag, parseArgs } from "../args.js";
|
|
3
3
|
import { importerClosure } from "../importers.js";
|
|
4
|
-
import { isUntyped, textualCount, textualHint, textualLine, textualMatches } from "./impact.js";
|
|
4
|
+
import { isPlainLocal, isUntyped, textualCount, textualHint, textualLine, textualMatches } from "./impact.js";
|
|
5
5
|
import { findOutsideProject, nearestTsconfig } from "../project.js";
|
|
6
6
|
import { viaLine } from "../references.js";
|
|
7
7
|
import { block, helpBlock, out } from "../render.js";
|
|
@@ -23,7 +23,7 @@ const VIA_DOC = `via_interface: n (Iface.member) = References through an interfa
|
|
|
23
23
|
refs_total: 0 under -P in a workspace lists found_outside_project[n]{file,line} = importers outside that Project
|
|
24
24
|
scope = in a monorepo without -P the program is the declaring file + its importers (scope_partial: true when the importer scan was capped)
|
|
25
25
|
tsconfig: x (n referenced configs) = a references-only tsconfig expanded to the configs it references
|
|
26
|
-
textual: n matches in m files = whole-word count of the name in Project files (JavaScript declarations and implicit projects); above refs_total = uses the checker cannot resolve - see refs --grep
|
|
26
|
+
textual: n matches in m files = whole-word count of the name in Project files (JavaScript declarations and implicit projects); 'n matches in this file' for a non-exported local; above refs_total = uses the checker cannot resolve - see refs --grep
|
|
27
27
|
templates: n usages in m files = template uses (.html .htm .ejs .hbs .njk .pug) of a directive or component (<wap-select / wap-select / data-wap-select), its bindings (fallback-display-property= on that tag), a controller (ng-controller) or a filter (| money); refs --grep lists them in unresolved`;
|
|
28
28
|
export const REFS_HELP = `usage: ts-axi refs <name | Class.member | path:line[:col]> [flags]
|
|
29
29
|
flags[12]:
|
|
@@ -150,7 +150,8 @@ async function run(args, ctx, invoked) {
|
|
|
150
150
|
const header = { ...warning, ...projectFields(opened), symbol, kind, declared_at: declaredAt };
|
|
151
151
|
const explicit = Boolean(project || opened.settings.project);
|
|
152
152
|
const declaredFile = decl?.file ?? (def && def.line > 0 ? def.file : undefined);
|
|
153
|
-
const
|
|
153
|
+
const localFile = isPlainLocal(decl) ? opened.index.abs(decl.file) : undefined;
|
|
154
|
+
const textual = declaredFile !== undefined && isUntyped(opened, decl, declaredFile) ? textualCount(opened, symbol.split(".").pop(), new Map(), localFile) : undefined;
|
|
154
155
|
const tplTarget = decl ? templateTarget(decl, opened.index.declarations(opened.index.abs(decl.file))) : undefined;
|
|
155
156
|
const templates = tplTarget ? templateRows(loadTemplates(opened.root), tplTarget) : undefined;
|
|
156
157
|
const o = { started, symbol, declaredAt, show, scope, explicit, label, name, absFile: resolved.absFile, ...(decl ? { decl } : {}), ...(textual ? { textual } : {}), ...(templates ? { templates } : {}) };
|
|
@@ -241,7 +242,7 @@ function viaFields(via, bases) {
|
|
|
241
242
|
}
|
|
242
243
|
/** Whole-word matches of the name on lines that hold no known position (what `--grep` lists). */
|
|
243
244
|
function unresolvedOf(opened, semantic, o, known) {
|
|
244
|
-
return textualMatches(opened, semantic, [o.symbol.split(".").pop()], new Set(known.map((r) => `${r.file}:${r.line}`)), new Set(o.decl ? [`${o.decl.file}:${o.decl.line}`, `${o.decl.file}:${o.decl.nameLine}`] : []), new Map());
|
|
245
|
+
return textualMatches(opened, semantic, [o.symbol.split(".").pop()], new Set(known.map((r) => `${r.file}:${r.line}`)), new Set(o.decl ? [`${o.decl.file}:${o.decl.line}`, `${o.decl.file}:${o.decl.nameLine}`] : []), new Map(), isPlainLocal(o.decl) ? new Map([[o.decl.name, opened.index.abs(o.decl.file)]]) : new Map());
|
|
245
246
|
}
|
|
246
247
|
/**
|
|
247
248
|
* `textual` field and, when the name occurs more often than refs_total and some matches sit on no
|
package/dist/src/symbols.js
CHANGED
|
@@ -232,6 +232,8 @@ const CONSTRUCTOR_VERBS = new Set(["controller", "service", "provider"]);
|
|
|
232
232
|
/** Router providers whose `.state(...)` / `.when(...)` calls declare a state (ui-router, ngRoute). */
|
|
233
233
|
const STATE_PROVIDER = /^\$?stateProvider$/i;
|
|
234
234
|
const ROUTE_PROVIDER = /^\$?routeProvider$/i;
|
|
235
|
+
/** Keys that, next to a string `name`, make an array element a ui-router state object. */
|
|
236
|
+
const STATE_KEYS = new Set(["url", "resolve", "component", "controller", "template", "templateUrl", "views", "abstract"]);
|
|
235
237
|
/** Directive definition keys whose object literal lists the bindings. */
|
|
236
238
|
const BINDING_KEYS = new Set(["scope", "bindToController", "bindings"]);
|
|
237
239
|
/** `Template.x.<hook>(fn)` lifecycle callbacks. */
|
|
@@ -460,40 +462,99 @@ export function collectDeclarations(sf, file) {
|
|
|
460
462
|
}
|
|
461
463
|
}
|
|
462
464
|
};
|
|
463
|
-
/**
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
465
|
+
/** Same-file string consts (`const base = 'main.x'`), for state names built from them; ambiguous names dropped. */
|
|
466
|
+
let stringConsts;
|
|
467
|
+
const constValue = (id, depth) => {
|
|
468
|
+
if (!stringConsts) {
|
|
469
|
+
stringConsts = new Map();
|
|
470
|
+
const visit = (node) => {
|
|
471
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
472
|
+
const init = unwrap(node.initializer);
|
|
473
|
+
if (isStringName(init) || ts.isTemplateExpression(init))
|
|
474
|
+
stringConsts.set(node.name.text, stringConsts.has(node.name.text) ? null : init);
|
|
475
|
+
}
|
|
476
|
+
ts.forEachChild(node, visit);
|
|
477
|
+
};
|
|
478
|
+
visit(sf);
|
|
479
|
+
}
|
|
480
|
+
const init = stringConsts.get(id);
|
|
481
|
+
return init && depth < 3 ? stateNameOf(init, depth + 1) : undefined;
|
|
482
|
+
};
|
|
483
|
+
/** A literal, a template literal (`${...}` = a same-file string const, else `*`) or a const identifier. */
|
|
484
|
+
const stateNameOf = (expr, depth = 0) => {
|
|
485
|
+
const e = unwrap(expr);
|
|
486
|
+
if (isStringName(e))
|
|
487
|
+
return e.text;
|
|
488
|
+
if (ts.isTemplateExpression(e)) {
|
|
489
|
+
return e.head.text + e.templateSpans.map((s) => (ts.isIdentifier(s.expression) ? (constValue(s.expression.text, depth) ?? "*") : "*") + s.literal.text).join("");
|
|
490
|
+
}
|
|
491
|
+
if (ts.isIdentifier(e))
|
|
492
|
+
return constValue(e.text, depth) ?? e.text;
|
|
493
|
+
return undefined;
|
|
494
|
+
};
|
|
495
|
+
const propOf = (obj, key) => obj.properties.find((p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && nameText(p.name, sf) === key);
|
|
496
|
+
/** `name` of a state object literal: `name: 'x'`, `` name: `${base}.x` ``, `name: base` or `{ name }`. */
|
|
497
|
+
const stateObjectName = (obj) => {
|
|
498
|
+
const prop = propOf(obj, "name");
|
|
499
|
+
if (prop && ts.isPropertyAssignment(prop))
|
|
500
|
+
return stateNameOf(prop.initializer);
|
|
501
|
+
if (prop && ts.isShorthandPropertyAssignment(prop))
|
|
502
|
+
return stateNameOf(prop.name);
|
|
503
|
+
return undefined;
|
|
504
|
+
};
|
|
505
|
+
/** A ui-router state object (`.state()` argument or array element) or an ngRoute config as a `state` Declaration plus its callbacks. */
|
|
506
|
+
const stateFromObject = (obj, name, start, end, route) => {
|
|
507
|
+
const url = propOf(obj, "url");
|
|
508
|
+
const urlText = url && ts.isPropertyAssignment(url) && isStringName(url.initializer) ? url.initializer.text : undefined;
|
|
477
509
|
const parent = route ? "routes" : "states";
|
|
478
510
|
// Plain-literal options are compared, not displayed: a new controller or template is a contract change.
|
|
479
511
|
const options = obj.properties
|
|
480
512
|
.filter((p) => ts.isPropertyAssignment(p) && nameText(p.name, sf) !== "name" && nameText(p.name, sf) !== "url")
|
|
481
513
|
.filter((p) => isStringName(p.initializer) || p.initializer.kind === ts.SyntaxKind.TrueKeyword || p.initializer.kind === ts.SyntaxKind.FalseKeyword)
|
|
482
514
|
.map((p) => tokenText(p, sf, p.getStart(sf), p.end));
|
|
483
|
-
const callee = call.expression;
|
|
484
515
|
nested({
|
|
485
|
-
start
|
|
486
|
-
end
|
|
516
|
+
start,
|
|
517
|
+
end,
|
|
487
518
|
kind: "state",
|
|
488
519
|
name,
|
|
489
520
|
parentName: parent,
|
|
490
521
|
exported: true,
|
|
491
|
-
signature: `${route ? "route" : "state"} ${name}${
|
|
522
|
+
signature: `${route ? "route" : "state"} ${name}${urlText !== undefined && !route ? ` (url ${urlText})` : ""}${options.length > 0 ? `\n${options.join("; ")}` : ""}`,
|
|
492
523
|
namePos: obj.getStart(sf),
|
|
493
524
|
nameless: true,
|
|
494
525
|
});
|
|
495
526
|
stateMembers(obj, `${parent}.${name}`, 1);
|
|
496
527
|
};
|
|
528
|
+
/** One `.state('name', {...})`, `.state({ name, ... })` or `.when('/path', {...})` call. */
|
|
529
|
+
const stateCall = (call, route) => {
|
|
530
|
+
const [first, second] = call.arguments;
|
|
531
|
+
const obj = first && ts.isObjectLiteralExpression(first) && !route ? first : second && ts.isObjectLiteralExpression(second) ? second : undefined;
|
|
532
|
+
if (!obj)
|
|
533
|
+
return;
|
|
534
|
+
const name = isStringName(first) ? first.text : route ? undefined : stateObjectName(obj);
|
|
535
|
+
if (name === undefined)
|
|
536
|
+
return;
|
|
537
|
+
stateFromObject(obj, name, call.expression.name.getStart(sf), call.end, route);
|
|
538
|
+
};
|
|
539
|
+
/** State objects among the elements of an array literal, or of `[...].concat([...], ...)`; other values ignored. */
|
|
540
|
+
const stateArray = (expr) => {
|
|
541
|
+
const e = unwrap(expr);
|
|
542
|
+
if (ts.isArrayLiteralExpression(e)) {
|
|
543
|
+
for (const el of e.elements) {
|
|
544
|
+
const obj = unwrap(el);
|
|
545
|
+
if (!ts.isObjectLiteralExpression(obj) || !obj.properties.some((p) => STATE_KEYS.has(nameText(p.name, sf) ?? "")))
|
|
546
|
+
continue;
|
|
547
|
+
const name = stateObjectName(obj);
|
|
548
|
+
if (name !== undefined)
|
|
549
|
+
stateFromObject(obj, name, obj.getStart(sf), obj.end, false);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
else if (ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) && e.expression.name.text === "concat") {
|
|
553
|
+
stateArray(e.expression.expression);
|
|
554
|
+
for (const arg of e.arguments)
|
|
555
|
+
stateArray(arg);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
497
558
|
/** `$stateProvider.state(...).state(...)` and `$routeProvider.when(...)` chains; `.otherwise()` ignored. */
|
|
498
559
|
const routerChain = (expr) => {
|
|
499
560
|
const calls = [];
|
|
@@ -567,8 +628,12 @@ export function collectDeclarations(sf, file) {
|
|
|
567
628
|
if (target === "module.exports") {
|
|
568
629
|
if (ts.isObjectLiteralExpression(right))
|
|
569
630
|
objectMembers(right, "exports", true);
|
|
631
|
+
else
|
|
632
|
+
stateArray(right);
|
|
570
633
|
return;
|
|
571
634
|
}
|
|
635
|
+
if (target === `exports.${left.name.text}` || target === `module.exports.${left.name.text}`)
|
|
636
|
+
stateArray(right);
|
|
572
637
|
const owner = left.expression;
|
|
573
638
|
if (ts.isPropertyAccessExpression(owner) && owner.name.text === "prototype" && ts.isIdentifier(owner.expression)) {
|
|
574
639
|
const cls = owner.expression.text;
|
|
@@ -614,6 +679,7 @@ export function collectDeclarations(sf, file) {
|
|
|
614
679
|
routerBody(fn.body);
|
|
615
680
|
const exported = inIife ? false : topLevelExported(name);
|
|
616
681
|
const init = unwrap(decl.initializer);
|
|
682
|
+
stateArray(init);
|
|
617
683
|
if (ts.isObjectLiteralExpression(init))
|
|
618
684
|
objectMembers(init, name, exported);
|
|
619
685
|
else if (fn && isConstructorName(name))
|
|
@@ -637,6 +703,9 @@ export function collectDeclarations(sf, file) {
|
|
|
637
703
|
else
|
|
638
704
|
callChain(expr);
|
|
639
705
|
}
|
|
706
|
+
else if (ts.isExportAssignment(statement)) {
|
|
707
|
+
stateArray(statement.expression);
|
|
708
|
+
}
|
|
640
709
|
else if (ts.isIfStatement(statement)) {
|
|
641
710
|
scriptStatement(statement.thenStatement, inIife);
|
|
642
711
|
if (statement.elseStatement)
|
package/dist/src/templates.js
CHANGED
|
@@ -53,6 +53,46 @@ export function loadTemplates(root) {
|
|
|
53
53
|
}
|
|
54
54
|
return files;
|
|
55
55
|
}
|
|
56
|
+
const TAG_SPAN_LINES = 40;
|
|
57
|
+
/**
|
|
58
|
+
* Text of the tag open at `offset` (from its `<`), or undefined when `offset` is outside a tag or
|
|
59
|
+
* inside a quoted value. Forward scan from up to 40 lines back; `>` inside quotes does not close.
|
|
60
|
+
* Tolerant of broken markup: never throws, an unclosed quote just ends the tag at the scan point.
|
|
61
|
+
*/
|
|
62
|
+
function openTagBefore(text, offset, lineStarts) {
|
|
63
|
+
let lo = 0;
|
|
64
|
+
let hi = lineStarts.length - 1;
|
|
65
|
+
while (lo < hi) {
|
|
66
|
+
const mid = (lo + hi + 1) >> 1;
|
|
67
|
+
if (lineStarts[mid] <= offset)
|
|
68
|
+
lo = mid;
|
|
69
|
+
else
|
|
70
|
+
hi = mid - 1;
|
|
71
|
+
}
|
|
72
|
+
let tagStart = -1;
|
|
73
|
+
let quote = "";
|
|
74
|
+
for (let i = lineStarts[Math.max(0, lo - TAG_SPAN_LINES)]; i < offset; i++) {
|
|
75
|
+
const c = text[i];
|
|
76
|
+
if (tagStart < 0) {
|
|
77
|
+
if (c === "<" && /[A-Za-z]/.test(text[i + 1] ?? ""))
|
|
78
|
+
tagStart = i;
|
|
79
|
+
}
|
|
80
|
+
else if (quote) {
|
|
81
|
+
if (c === quote)
|
|
82
|
+
quote = "";
|
|
83
|
+
}
|
|
84
|
+
else if (c === '"' || c === "'") {
|
|
85
|
+
quote = c;
|
|
86
|
+
}
|
|
87
|
+
else if (c === ">") {
|
|
88
|
+
tagStart = -1;
|
|
89
|
+
}
|
|
90
|
+
else if (c === "<" && /[A-Za-z]/.test(text[i + 1] ?? "")) {
|
|
91
|
+
tagStart = i;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return tagStart < 0 || quote ? undefined : text.slice(tagStart, offset);
|
|
95
|
+
}
|
|
56
96
|
/** Usages of `target` in one template text: at most one per line. */
|
|
57
97
|
export function usagesIn(text, target) {
|
|
58
98
|
const result = [];
|
|
@@ -66,12 +106,11 @@ export function usagesIn(text, target) {
|
|
|
66
106
|
const offset = (m.index ?? 0) + (/^[<\s"'|]/.test(m[0]) ? 1 : 0);
|
|
67
107
|
if (target.owner) {
|
|
68
108
|
// The attribute counts only inside a tag opened by (or carrying) the owner directive.
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
109
|
+
const tag = openTagBefore(text, offset, lineStarts);
|
|
110
|
+
if (tag === undefined)
|
|
71
111
|
continue;
|
|
72
|
-
const tag = text.slice(tagStart, offset);
|
|
73
112
|
target.owner.lastIndex = 0;
|
|
74
|
-
if (
|
|
113
|
+
if (!target.owner.test(tag))
|
|
75
114
|
continue;
|
|
76
115
|
}
|
|
77
116
|
let lo = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-axi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "AXI-compliant TypeScript symbol CLI for agents - definitions, references, callers, file outlines and change impact (blast radius) with token-efficient TOON output",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|