@expo/code-review-cli 0.9.0 → 0.9.2
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/build/commands/ref-check.js +1 -1
- package/build/core/config-refs.js +48 -15
- package/package.json +1 -1
|
@@ -22,7 +22,7 @@ Exit 0 = every ref holds. Exit 1 = at least one is broken.
|
|
|
22
22
|
|
|
23
23
|
Options:
|
|
24
24
|
--root <dir> Repository root to check (default: the current git repo).
|
|
25
|
-
--json Emit {ok, problems:[{file, line, kind, problem}]} on stdout.
|
|
25
|
+
--json Emit {ok, problems:[{file, line, kind, problem, target?}]} on stdout.
|
|
26
26
|
`;
|
|
27
27
|
const KIND_LABEL = {
|
|
28
28
|
"broken-ref": "broken ref",
|
|
@@ -7,6 +7,7 @@ import { CONFIG_DIRNAME, stripJsonComments, stripTrailingCommas } from "../confi
|
|
|
7
7
|
import { ROUTING_FILENAME } from "../config/routing.js";
|
|
8
8
|
import { git, pathInside } from "./exec.js";
|
|
9
9
|
import { matchesIgnore } from "./noise.js";
|
|
10
|
+
import { isAmbientRuntimeConfig } from "./scrub.js";
|
|
10
11
|
/**
|
|
11
12
|
* Built by concatenation so this module's own regexes and doc comments are not
|
|
12
13
|
* themselves collected as refs by a scanner (the repo's `ref-check` does the same).
|
|
@@ -40,6 +41,7 @@ const CITATION_EXTENSIONS = new Set([
|
|
|
40
41
|
".css",
|
|
41
42
|
".go",
|
|
42
43
|
".graphql",
|
|
44
|
+
".hcl",
|
|
43
45
|
".h",
|
|
44
46
|
".hpp",
|
|
45
47
|
".html",
|
|
@@ -63,6 +65,8 @@ const CITATION_EXTENSIONS = new Set([
|
|
|
63
65
|
".sh",
|
|
64
66
|
".sql",
|
|
65
67
|
".swift",
|
|
68
|
+
".tf",
|
|
69
|
+
".tfvars",
|
|
66
70
|
".toml",
|
|
67
71
|
".ts",
|
|
68
72
|
".tsx",
|
|
@@ -608,10 +612,11 @@ export async function checkConfigRefs(options) {
|
|
|
608
612
|
// Does an extensionless token name something real? Cached: prompts repeat the same
|
|
609
613
|
// paths, and each miss would otherwise cost a stat per occurrence.
|
|
610
614
|
const resolvable = new Map();
|
|
611
|
-
/** The root-relative path
|
|
615
|
+
/** The root-relative path a token names, or null. Scope first: a scoped prompt
|
|
616
|
+
* citing `alerts/` means its own tree even when the root has one too. */
|
|
612
617
|
const namedPath = async (token, scopeRoot) => {
|
|
613
618
|
for (const candidate of pathishCandidates(token)) {
|
|
614
|
-
for (const base of [
|
|
619
|
+
for (const base of [scopeRoot, root]) {
|
|
615
620
|
const absolute = path.resolve(base, candidate);
|
|
616
621
|
let resolved = resolvable.get(absolute);
|
|
617
622
|
if (resolved === undefined) {
|
|
@@ -630,6 +635,18 @@ export async function checkConfigRefs(options) {
|
|
|
630
635
|
}
|
|
631
636
|
return null;
|
|
632
637
|
};
|
|
638
|
+
/** The glob a wildcard token names, rebased onto the scope when that is what
|
|
639
|
+
* matches, so the suggested `glob:` ref actually resolves. */
|
|
640
|
+
const namedGlob = (token, scopeRoot) => {
|
|
641
|
+
const scopeRel = path.relative(root, scopeRoot).split(path.sep).join("/");
|
|
642
|
+
const candidates = [...(scopeRel && scopeRel !== "." ? [`${scopeRel}/${token}`] : []), token];
|
|
643
|
+
for (const candidate of candidates) {
|
|
644
|
+
if (index.files.some((file) => matchesIgnore(file, candidate))) {
|
|
645
|
+
return candidate;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return null;
|
|
649
|
+
};
|
|
633
650
|
for (const dir of dirs) {
|
|
634
651
|
const scopeRoot = path.dirname(dir);
|
|
635
652
|
for (const file of await setupFiles(dir)) {
|
|
@@ -658,6 +675,7 @@ export async function checkConfigRefs(options) {
|
|
|
658
675
|
line: ref.line,
|
|
659
676
|
kind: problem.startsWith("cites a line number") ? "line-number-ref" : "broken-ref",
|
|
660
677
|
problem,
|
|
678
|
+
target: ref.target,
|
|
661
679
|
});
|
|
662
680
|
continue;
|
|
663
681
|
}
|
|
@@ -674,24 +692,28 @@ export async function checkConfigRefs(options) {
|
|
|
674
692
|
}
|
|
675
693
|
}
|
|
676
694
|
for (const { line, token } of findProseCitations(text)) {
|
|
677
|
-
const
|
|
695
|
+
const isWild = token.includes("*");
|
|
696
|
+
const named = isWild ? namedGlob(token, scopeRoot) : await namedPath(token, scopeRoot);
|
|
697
|
+
// Shape alone makes a citation; otherwise it must name something real, or
|
|
698
|
+
// `anthropic/claude-opus-5` would read as a path.
|
|
699
|
+
if (!isCodeCitation(token) && !named) {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
// Coverage accepts the resolved path too, else the only ref that silences a
|
|
703
|
+
// scope-relative citation is an over-broad `glob:**/<basename>`.
|
|
704
|
+
const forms = [...citationForms(token), ...(named ? coveringForms(named) : [])];
|
|
678
705
|
if (ignored.has(token) ||
|
|
679
706
|
forms.some((form) => covered.has(form)) ||
|
|
680
707
|
coveringGlobs.some((pattern) => forms.some((form) => matchesIgnore(form.replace(/^\.\.\.\//, ""), pattern)))) {
|
|
681
708
|
continue;
|
|
682
709
|
}
|
|
683
|
-
// Extension or wildcard tail ⇒ a citation on shape alone. Otherwise it only
|
|
684
|
-
// counts if it names something real: `eas-build-worker/terraform` and
|
|
685
|
-
// `general-central/{module,production}` are paths, `anthropic/claude-opus-5`
|
|
686
|
-
// is shaped identically and is not.
|
|
687
|
-
const named = isCodeCitation(token) ? null : await namedPath(token, scopeRoot);
|
|
688
|
-
if (!isCodeCitation(token) && !named) {
|
|
689
|
-
continue;
|
|
690
|
-
}
|
|
691
710
|
const lineCitation = LINE_CITATION_RE.exec(token);
|
|
692
|
-
//
|
|
693
|
-
|
|
694
|
-
|
|
711
|
+
// A wildcard token means the family; anything else gets the precise resolved path.
|
|
712
|
+
const suggestion = isWild
|
|
713
|
+
? named
|
|
714
|
+
? `${GLOB_PREFIX}${named}`
|
|
715
|
+
: suggestedRef(token)
|
|
716
|
+
: (named ?? suggestedRef(token));
|
|
695
717
|
problems.push({
|
|
696
718
|
file: relative,
|
|
697
719
|
line,
|
|
@@ -699,6 +721,7 @@ export async function checkConfigRefs(options) {
|
|
|
699
721
|
problem: lineCitation
|
|
700
722
|
? `\`${token}\` pins a line number; cite the file or a \`#symbol\` instead, as \`${REF_MARK} ${suggestedRef(lineCitation[1])}\``
|
|
701
723
|
: `\`${token}\` cites code without a ref; add \`${REF_MARK} ${suggestion} — why it matters\` (or \`${IGNORE_MARK} ${token}\` if it is not a path)`,
|
|
724
|
+
target: token,
|
|
702
725
|
});
|
|
703
726
|
}
|
|
704
727
|
}
|
|
@@ -720,6 +743,12 @@ export async function checkConfigRefs(options) {
|
|
|
720
743
|
citedPaths: [...citedPaths].sort(),
|
|
721
744
|
};
|
|
722
745
|
}
|
|
746
|
+
/** The file a target names, with any `glob:` prefix, `#anchor` or `:line` stripped. */
|
|
747
|
+
function citedBasename(target) {
|
|
748
|
+
const withoutPrefix = target.startsWith(GLOB_PREFIX) ? target.slice(GLOB_PREFIX.length) : target;
|
|
749
|
+
const withoutLine = LINE_CITATION_RE.exec(withoutPrefix)?.[1] ?? withoutPrefix;
|
|
750
|
+
return path.basename(splitAnchor(withoutLine)[0]);
|
|
751
|
+
}
|
|
723
752
|
/** How many examples a review-side note names before saying "and N more". */
|
|
724
753
|
const NOTE_EXAMPLES = 5;
|
|
725
754
|
function andMore(items) {
|
|
@@ -742,7 +771,11 @@ export async function reviewSetupRefNotes(options) {
|
|
|
742
771
|
return []; // a check that cannot run must never degrade the review
|
|
743
772
|
}
|
|
744
773
|
const notes = [];
|
|
745
|
-
|
|
774
|
+
// The read root is scrubbed of ambient runtime config (AGENTS.md, CLAUDE.md, .env…),
|
|
775
|
+
// so a ref to one is missing by design here, not stale. The template itself cites
|
|
776
|
+
// AGENTS.md, which would make this note cry wolf on every PR.
|
|
777
|
+
const broken = report.problems.filter((problem) => problem.kind !== "unannotated-citation" &&
|
|
778
|
+
!(problem.target && isAmbientRuntimeConfig(citedBasename(problem.target))));
|
|
746
779
|
if (broken.length > 0) {
|
|
747
780
|
notes.push(`The reviewer setup cites code that no longer resolves (${broken.length} ref(s)): ` +
|
|
748
781
|
`${andMore(broken.map((problem) => `${problem.file}:${problem.line}`))}. ` +
|