@expo/code-review-cli 0.9.1 → 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 +44 -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,18 +692,15 @@ export async function checkConfigRefs(options) {
|
|
|
674
692
|
}
|
|
675
693
|
}
|
|
676
694
|
for (const { line, token } of findProseCitations(text)) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
const named = isCodeCitation(token) ? null : await namedPath(token, scopeRoot);
|
|
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.
|
|
682
699
|
if (!isCodeCitation(token) && !named) {
|
|
683
700
|
continue;
|
|
684
701
|
}
|
|
685
|
-
// Coverage
|
|
686
|
-
//
|
|
687
|
-
// `infrastructure/cert-manager/`. Without this, the fix the message suggests
|
|
688
|
-
// does not silence the citation it was suggested for.
|
|
702
|
+
// Coverage accepts the resolved path too, else the only ref that silences a
|
|
703
|
+
// scope-relative citation is an over-broad `glob:**/<basename>`.
|
|
689
704
|
const forms = [...citationForms(token), ...(named ? coveringForms(named) : [])];
|
|
690
705
|
if (ignored.has(token) ||
|
|
691
706
|
forms.some((form) => covered.has(form)) ||
|
|
@@ -693,9 +708,12 @@ export async function checkConfigRefs(options) {
|
|
|
693
708
|
continue;
|
|
694
709
|
}
|
|
695
710
|
const lineCitation = LINE_CITATION_RE.exec(token);
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
|
|
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));
|
|
699
717
|
problems.push({
|
|
700
718
|
file: relative,
|
|
701
719
|
line,
|
|
@@ -703,6 +721,7 @@ export async function checkConfigRefs(options) {
|
|
|
703
721
|
problem: lineCitation
|
|
704
722
|
? `\`${token}\` pins a line number; cite the file or a \`#symbol\` instead, as \`${REF_MARK} ${suggestedRef(lineCitation[1])}\``
|
|
705
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,
|
|
706
725
|
});
|
|
707
726
|
}
|
|
708
727
|
}
|
|
@@ -724,6 +743,12 @@ export async function checkConfigRefs(options) {
|
|
|
724
743
|
citedPaths: [...citedPaths].sort(),
|
|
725
744
|
};
|
|
726
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
|
+
}
|
|
727
752
|
/** How many examples a review-side note names before saying "and N more". */
|
|
728
753
|
const NOTE_EXAMPLES = 5;
|
|
729
754
|
function andMore(items) {
|
|
@@ -746,7 +771,11 @@ export async function reviewSetupRefNotes(options) {
|
|
|
746
771
|
return []; // a check that cannot run must never degrade the review
|
|
747
772
|
}
|
|
748
773
|
const notes = [];
|
|
749
|
-
|
|
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))));
|
|
750
779
|
if (broken.length > 0) {
|
|
751
780
|
notes.push(`The reviewer setup cites code that no longer resolves (${broken.length} ref(s)): ` +
|
|
752
781
|
`${andMore(broken.map((problem) => `${problem.file}:${problem.line}`))}. ` +
|