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 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
- /** Whole-word matches of `names` in Project files and templates that are not `known` References. */
235
- export function textualMatches(opened, semantic, names, known, skipLines, changeSet) {
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
- /** Whole-word occurrences of `name` in Project files outside `changeSet`; lines declaring a `name` excluded. */
271
- export function textualCount(opened, name, changeSet = new Map()) {
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.textual = textualCount(opened, row.live.name, changeSet);
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 name = (r.change.after ?? r.change.before).name;
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 textual = declaredFile !== undefined && isUntyped(opened, decl, declaredFile) ? textualCount(opened, symbol.split(".").pop()) : undefined;
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
@@ -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
- /** One `.state('name', {...})`, `.state({ name, ... })` or `.when('/path', {...})` call. */
464
- const stateCall = (call, route) => {
465
- const [first, second] = call.arguments;
466
- const obj = first && ts.isObjectLiteralExpression(first) && !route ? first : second && ts.isObjectLiteralExpression(second) ? second : undefined;
467
- if (!obj)
468
- return;
469
- const literal = (key) => {
470
- const prop = obj.properties.find((p) => ts.isPropertyAssignment(p) && nameText(p.name, sf) === key);
471
- return prop && isStringName(prop.initializer) ? prop.initializer.text : undefined;
472
- };
473
- const name = isStringName(first) ? first.text : literal("name");
474
- if (name === undefined)
475
- return;
476
- const url = literal("url");
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: callee.name.getStart(sf),
486
- end: call.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}${url !== undefined && !route ? ` (url ${url})` : ""}${options.length > 0 ? `\n${options.join("; ")}` : ""}`,
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)
@@ -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 tagStart = text.lastIndexOf("<", offset);
70
- if (tagStart < 0)
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 (tag.includes(">") || !target.owner.test(tag))
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.6",
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": [