@danielsimonjr/repo-tools 2.2.1 → 2.2.3

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/CHANGELOG.md CHANGED
@@ -6,6 +6,67 @@ All notable changes to this project are recorded in this file. The format follow
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.2.3] - 2026-10-16
10
+
11
+ ### Fixed
12
+
13
+ - `repo-tools map` reads a `.tsx` or `.jsx` file with the TSX grammar. It read the file with the TypeScript grammar,
14
+ which cannot read JSX. That grammar gives error nodes, and the export statements that survive the error recovery
15
+ depend on the text of the file. The same component file gave 1 export without doc comments and 2 exports with doc
16
+ comments. In one application repository of 746 source files, the export count rises from 3259 to 3406, and the
17
+ count is the same with and without the doc comments. A drift gate (`repo-tools check`) that holds the earlier count
18
+ fails until `repo-tools map` writes the reports again.
19
+ A `.js`, `.mjs` or `.cjs` file that holds JSX still uses the TypeScript grammar.
20
+
21
+ ## [2.2.2] - 2026-10-15
22
+
23
+ ### Changed
24
+
25
+ - The workflows pin three GitHub Actions to a newer release: `actions/upload-artifact` v7.0.1 to v7.0.2 (in `ci.yml` and
26
+ `build.yml`), `actions/download-artifact` v8.0.1 to v8.0.2 (in `build.yml`) and `actions/setup-node` v7.0.0 to v7.1.0 (in
27
+ `ci.yml`). Each pin is the commit of the upstream release tag. `build.yml` runs only on a `v*` tag, so the next release
28
+ build is the first run of the new `download-artifact` pin.
29
+
30
+ ### Fixed
31
+
32
+ - `repo-tools docs` reads a `.tsx` or `.jsx` file that has a bare `&` in JSX text (`<h3>Review & Start</h3>`).
33
+ React and `tsc` accept the text, and the grammar gave an error node, so the file was an unparsed file and its
34
+ symbols were not measured. The analyser now writes a space over a `&` that is the first character of an error
35
+ node directly inside a JSX element, which keeps the length, and parses again. Any other error stays an error.
36
+ A real syntax error beside such a text is still reported. In one application repository four files moved from
37
+ unparsed to measured, and two files with `import()` type expressions stay unparsed.
38
+ - `repo-tools docs check` reads the documentation of an overload set from its first overload. Before, the analyser read only
39
+ the comment that directly precedes a declaration, so an implementation signature that follows overload signatures counted
40
+ as undocumented (M1), although a caller sees only the overloads. A repository then needed a comment that no reader sees.
41
+ The implementation signature now takes the doc of the first overload of the same name, in a function set and in a class
42
+ member set. A doc that directly precedes the implementation still wins. A statement between the overloads and the
43
+ implementation ends the set. The `;` that ends a member signature in a class body does not end the set.
44
+ Rule M3 still compares the documented parameters with the implementation.
45
+ - `repo-tools docs` reads a file that has an `import("m")` type expression (`let v: import("fs").Dirent[]` and
46
+ `f<typeof import("m")>()`). `tsc` accepts both. The grammar reads the call as a member expression of a value, so it
47
+ cannot take `[]` after it, and it reads the call in a type argument as a comparison. Both gave error nodes, so the
48
+ file was an unparsed file. When the first parse has an error, the analyser now writes an identifier of equal length
49
+ over each `import("m")` call that holds a string literal, keeps its line breaks, and parses again. A file that
50
+ parses is never rewritten, and any other error stays an error. In the application repository of the earlier entry
51
+ the last two unparsed files move to measured.
52
+ - Two fan-out tests (`map-graph.test.ts` and `map-query.test.ts`) have an explicit 30 s timeout. Each builds a graph of about
53
+ 9,000 nodes. The graph test took 0.85 s on a fast machine and 4.57 s on a slower one, against bun's 5 s default, so it
54
+ failed there under any load. The tests check a step bound, not time, so only the timeout changes. A comment on each test records the measurement and
55
+ names no machine, because the privacy check rejects machine names.
56
+ - The privacy check accepts one finding in the history: the squash commit of #18 (`46a45d4c19af`) carries a
57
+ `Co-authored-by` trailer that GitHub added for the maintainer identity on the pull request branch, at line 53 of the
58
+ message. A commit message cannot change after a merge, so `scripts/privacy-accepted.txt` holds the entry, as it does
59
+ for `fc930a32f5d2`. Before, the `privacy` job and the test `this repository scans clean` failed on `main` after the
60
+ merge, although the pull request was green. A squash whose commits are all by the pull request opener adds no trailer.
61
+ - `repo-tools docs` keeps the doc of a declaration when a tool directive stands between the doc comment and the
62
+ declaration (`// eslint-disable-next-line`, `// @ts-expect-error`, `// prettier-ignore`, `// biome-ignore` and the
63
+ other directives of a closed list). Before, the directive hid the doc and the symbol counted as undocumented (M1),
64
+ although a directive applies to the next line and so must stay directly above the declaration; a React repository
65
+ with the `react-refresh` lint rule had four such hooks and components. A plain line comment can move, so it still
66
+ separates a doc comment from its declaration. `docs stub` also writes
67
+ a new block above the directive and not between the directive and the declaration, where the directive would apply
68
+ to the block.
69
+
9
70
  ## [2.2.1] - 2026-10-08
10
71
 
11
72
  ### Fixed
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  | `query` | Answers structural questions from the graph of `map`, and writes two derived reports. |
14
14
  | `ste` | Checks Markdown or docstring prose against Simplified Technical English. |
15
15
 
16
- Status: version 2.2.1.
16
+ Status: version 2.2.3.
17
17
 
18
18
  - `map`, `check` and `query` read their settings from `repo-tools.config.json` in the root. The
19
19
  section of `map` is `map` (its old name, `depgraph`, is also read). `--config=<file>` makes any
@@ -41,7 +41,7 @@ repo-tools --version
41
41
 
42
42
  The executable holds the tool and the Bun runtime. It needs no Bun, Node or `node_modules`.
43
43
  Download the file for your platform and `SHA256SUMS` from the
44
- [GitHub release](https://github.com/danielsimonjr/repo-tools/releases/tag/v2.2.1):
44
+ [GitHub release](https://github.com/danielsimonjr/repo-tools/releases/tag/v2.2.3):
45
45
 
46
46
  | Platform | File |
47
47
  |---|---|
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // package.json
3
3
  var package_default = {
4
4
  name: "@danielsimonjr/repo-tools",
5
- version: "2.2.1",
5
+ version: "2.2.3",
6
6
  description: "One CLI for repository analysis: dependency graphs creation and query with per-export API facts, file chunking, and context compression. TypeScript on Bun, compiled to a single executable.",
7
7
  type: "module",
8
8
  license: "MIT",
@@ -10895,6 +10895,9 @@ function loadGrammar(name2) {
10895
10895
  }
10896
10896
  return pending;
10897
10897
  }
10898
+ function grammarFor(path) {
10899
+ return path.endsWith(".tsx") || path.endsWith(".jsx") ? "tsx" : "typescript";
10900
+ }
10898
10901
  function parserFor(name2) {
10899
10902
  const parser = parsers.get(name2);
10900
10903
  if (!parser)
@@ -11122,8 +11125,8 @@ function addDynamicImportEdges(mod, calls) {
11122
11125
  });
11123
11126
  }
11124
11127
  }
11125
- function parseTs(source) {
11126
- const tree = parserFor("typescript").parse(source);
11128
+ function parseTs(source, path = "") {
11129
+ const tree = parserFor(grammarFor(path)).parse(source);
11127
11130
  if (!tree)
11128
11131
  throw new Error("tree-sitter returned no tree");
11129
11132
  const mod = emptyModule();
@@ -12568,6 +12571,8 @@ async function buildGraph(root) {
12568
12571
  }
12569
12572
  if (language === "typescript" || language === "python")
12570
12573
  await loadGrammar(language);
12574
+ if (language === "typescript" && found.some((f) => grammarFor(f.path) === "tsx"))
12575
+ await loadGrammar("tsx");
12571
12576
  const resolver = getResolver(language);
12572
12577
  const read = READERS[language];
12573
12578
  const parsed = new Map;
@@ -12576,7 +12581,7 @@ async function buildGraph(root) {
12576
12581
  const text = readSource(join11(root, f.path)).replace(/\r\n?/g, `
12577
12582
  `);
12578
12583
  sources.set(f.path, text);
12579
- parsed.set(f.path, read(text));
12584
+ parsed.set(f.path, read(text, f.path));
12580
12585
  }
12581
12586
  resolver.indexNamespaces?.(parsed);
12582
12587
  const files = new Map;
@@ -18164,9 +18169,6 @@ var DECLARATIONS = {
18164
18169
  method_definition: "method"
18165
18170
  };
18166
18171
  var DECLARATION_TYPES = Object.keys(DECLARATIONS);
18167
- function grammarFor(path) {
18168
- return path.endsWith(".tsx") || path.endsWith(".jsx") ? "tsx" : "typescript";
18169
- }
18170
18172
  var SPACE_CLASS = "[ \\t\\n\\r\\f\\v]";
18171
18173
  var ABSTRACT_PROPERTY = new RegExp(`\\babstract(${SPACE_CLASS}*\\??${SPACE_CLASS}*:)`, "g");
18172
18174
  var EXPORT_TYPE_STAR = new RegExp(`(\\bexport${SPACE_CLASS}+)type(${SPACE_CLASS}*\\*)`, "g");
@@ -18208,13 +18210,60 @@ function anchorOf(node) {
18208
18210
  target = target.parent;
18209
18211
  return target;
18210
18212
  }
18211
- function leadingDoc(anchor, text) {
18212
- const prev = anchor.previousSibling;
18213
+ var DIRECTIVE = new RegExp(`^//${SPACE_CLASS}*(?:eslint-(?:disable|enable)(?:-next-line|-line)?|@ts-(?:ignore|expect-error|nocheck|check)|` + "prettier-ignore|biome-ignore|oxlint-disable|deno-lint-ignore|tslint:(?:disable|enable)|" + `(?:istanbul|c8|v8)${SPACE_CLASS}+ignore)(?![\\w-])`);
18214
+ function isDirective(node, text) {
18215
+ return node.type === "comment" && DIRECTIVE.test(text(node));
18216
+ }
18217
+ function docBefore(anchor, text) {
18218
+ let prev = anchor.previousSibling;
18219
+ while (prev && isDirective(prev, text))
18220
+ prev = prev.previousSibling;
18213
18221
  if (prev?.type !== "comment")
18214
18222
  return "";
18215
18223
  const comment = text(prev);
18216
18224
  return comment.startsWith("/**") ? comment : "";
18217
18225
  }
18226
+ function stubAnchor(anchor, source, text) {
18227
+ let first = anchor;
18228
+ for (let prev = anchor.previousSibling;prev && isDirective(prev, text); ) {
18229
+ if (prev.endPosition.row + 1 < first.startPosition.row)
18230
+ break;
18231
+ const lineStart = source.lastIndexOf(`
18232
+ `, prev.startIndex - 1) + 1;
18233
+ if (source.slice(lineStart, prev.startIndex).trim() !== "")
18234
+ break;
18235
+ first = prev;
18236
+ prev = prev.previousSibling;
18237
+ }
18238
+ return first;
18239
+ }
18240
+ var SIGNATURES = new Set(["function_signature", "method_signature"]);
18241
+ function signatureName(sibling, text) {
18242
+ const inner = sibling.type === "export_statement" ? sibling.childForFieldName("declaration") : sibling;
18243
+ if (!inner || !SIGNATURES.has(inner.type))
18244
+ return null;
18245
+ const name2 = inner.childForFieldName("name");
18246
+ return name2 ? text(name2) : null;
18247
+ }
18248
+ function leadingDoc(anchor, node, text) {
18249
+ const own = docBefore(anchor, text);
18250
+ if (own || node.type !== "function_declaration" && node.type !== "method_definition") {
18251
+ return own;
18252
+ }
18253
+ const name2 = node.childForFieldName("name");
18254
+ if (!name2)
18255
+ return "";
18256
+ const implementation = text(name2);
18257
+ let first = null;
18258
+ for (let sibling = anchor.previousSibling;sibling; sibling = sibling.previousSibling) {
18259
+ if (sibling.type === "comment" || sibling.type === ";")
18260
+ continue;
18261
+ if (signatureName(sibling, text) !== implementation)
18262
+ break;
18263
+ first = sibling;
18264
+ }
18265
+ return first ? docBefore(first, text) : "";
18266
+ }
18218
18267
  function isExported2(node) {
18219
18268
  let parent = node.parent;
18220
18269
  while (parent) {
@@ -18240,8 +18289,53 @@ function proseOfComment(doc) {
18240
18289
  return proseOf(lines.join(`
18241
18290
  `));
18242
18291
  }
18292
+ var JSX_PARENTS = new Set(["jsx_element", "jsx_fragment"]);
18293
+ var MAX_REPAIRS = 16;
18294
+ var IMPORT_CALL = new RegExp(`\\bimport${SPACE_CLASS}*\\(${SPACE_CLASS}*(?:"[^"\\n]*"|'[^'\\n]*')${SPACE_CLASS}*\\)`, "g");
18295
+ function blankImportCalls(text) {
18296
+ return text.replace(IMPORT_CALL, (call) => {
18297
+ let name2 = true;
18298
+ let out2 = "";
18299
+ for (const ch of call) {
18300
+ if (/\s/u.test(ch)) {
18301
+ name2 = false;
18302
+ out2 += ch;
18303
+ } else {
18304
+ out2 += (name2 ? "_" : " ").repeat(ch.length);
18305
+ }
18306
+ }
18307
+ return out2;
18308
+ });
18309
+ }
18310
+ function parseSource(parser, text) {
18311
+ let current = sanitise(text);
18312
+ let tree = parser.parse(current);
18313
+ let importsBlanked = false;
18314
+ for (let pass = 0;tree?.rootNode.hasError && pass < MAX_REPAIRS; pass++) {
18315
+ const marks = [];
18316
+ for (const node of tree.rootNode.descendantsOfType("ERROR")) {
18317
+ const start2 = node.startIndex;
18318
+ if (current[start2] === "&" && node.parent && JSX_PARENTS.has(node.parent.type)) {
18319
+ marks.push(start2);
18320
+ }
18321
+ }
18322
+ let next = current;
18323
+ for (const at of marks)
18324
+ next = `${next.slice(0, at)} ${next.slice(at + 1)}`;
18325
+ if (!importsBlanked) {
18326
+ importsBlanked = true;
18327
+ next = blankImportCalls(next);
18328
+ }
18329
+ if (next === current)
18330
+ break;
18331
+ tree.delete();
18332
+ current = next;
18333
+ tree = parser.parse(current);
18334
+ }
18335
+ return tree;
18336
+ }
18243
18337
  function parsesAsTypeScript(path, text) {
18244
- const tree = parserFor(grammarFor(path)).parse(sanitise(text.replace(/^\uFEFF/, "")));
18338
+ const tree = parseSource(parserFor(grammarFor(path)), text.replace(/^\uFEFF/, ""));
18245
18339
  if (!tree)
18246
18340
  return false;
18247
18341
  try {
@@ -18254,7 +18348,7 @@ async function analyseTypeScript(path, source) {
18254
18348
  const report = newReport(path, "typescript");
18255
18349
  const grammar = grammarFor(path);
18256
18350
  await loadGrammar(grammar);
18257
- const tree = parserFor(grammar).parse(sanitise(source));
18351
+ const tree = parseSource(parserFor(grammar), source);
18258
18352
  if (!tree) {
18259
18353
  report.error = "parse error (the parser returned no tree)";
18260
18354
  return report;
@@ -18268,7 +18362,8 @@ async function analyseTypeScript(path, source) {
18268
18362
  for (const node of tree.rootNode.descendantsOfType(DECLARATION_TYPES)) {
18269
18363
  const kind = DECLARATIONS[node.type];
18270
18364
  const anchor = anchorOf(node);
18271
- const doc = leadingDoc(anchor, text);
18365
+ const lead = stubAnchor(anchor, source, text);
18366
+ const doc = leadingDoc(anchor, node, text);
18272
18367
  const dialect = dialectOf(doc);
18273
18368
  const typeLike = kind === "class" || kind === "interface" || kind === "type";
18274
18369
  const line = node.startPosition.row + 1;
@@ -18276,8 +18371,8 @@ async function analyseTypeScript(path, source) {
18276
18371
  const sym = {
18277
18372
  file: path,
18278
18373
  line,
18279
- anchorLine: anchor.startPosition.row + 1,
18280
- anchorColumn: anchor.startPosition.column,
18374
+ anchorLine: lead.startPosition.row + 1,
18375
+ anchorColumn: lead.startPosition.column,
18281
18376
  name: nameNode ? text(nameNode) : "<anonymous>",
18282
18377
  kind,
18283
18378
  exported: isExported2(node),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danielsimonjr/repo-tools",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "One CLI for repository analysis: dependency graphs creation and query with per-export API facts, file chunking, and context compression. TypeScript on Bun, compiled to a single executable.",
5
5
  "type": "module",
6
6
  "license": "MIT",