mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Merge branch 'release/v1.45.0' into feat/v145-issues
This commit is contained in:
commit
08675a4f13
41 files changed
+437
-239
No files matched your search
@@ -14,10 +14,9 @@
|
||||
* those.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -30,7 +29,7 @@ function sourceFiles(): string[] {
|
||||
}
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
return readSourceText(join(SRC, rel));
|
||||
}
|
||||
|
||||
function filesMatching(re: RegExp): string[] {
|
||||
|
||||
@@ -26,14 +26,17 @@
|
||||
* another module would slip it; `AI_ROUTE_PATTERNS` lists the literal shapes
|
||||
* the tree uses today.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { AiCapabilityKey } from "@/lib/ai/capabilities/types";
|
||||
|
||||
import { stripComments, walkSourceFiles } from "./helpers/source-files";
|
||||
import {
|
||||
readSourceText,
|
||||
stripComments,
|
||||
walkSourceFiles,
|
||||
} from "./helpers/source-files";
|
||||
|
||||
const SRC = path.resolve(__dirname, "..");
|
||||
|
||||
@@ -64,7 +67,7 @@ const FEATURE_FLAG_SWEEP = NON_TEST_FILES.filter(
|
||||
);
|
||||
|
||||
const read = (rel: string) =>
|
||||
stripComments(readFileSync(path.join(SRC, rel), "utf8"));
|
||||
stripComments(readSourceText(path.join(SRC, rel)));
|
||||
|
||||
/** Request literals of routes that call a model or serve model text. */
|
||||
const AI_ROUTE_PATTERNS: readonly RegExp[] = [
|
||||
|
||||
@@ -271,10 +271,17 @@ function delegateName(model: string): string {
|
||||
* route outright: the guard stayed green on all twelve checks, because the
|
||||
* cycle file was answering a question about a different table.
|
||||
*/
|
||||
const parentsCache = new Map<
|
||||
string,
|
||||
Array<{ field: string; parent: string }>
|
||||
>();
|
||||
|
||||
function relationParents(model: string): Array<{
|
||||
field: string;
|
||||
parent: string;
|
||||
}> {
|
||||
const cached = parentsCache.get(model);
|
||||
if (cached) return cached;
|
||||
const found: Array<{ field: string; parent: string }> = [];
|
||||
for (const [parent, body] of modelBodies()) {
|
||||
for (const line of body.split("\n")) {
|
||||
@@ -284,6 +291,7 @@ function relationParents(model: string): Array<{
|
||||
}
|
||||
}
|
||||
}
|
||||
parentsCache.set(model, found);
|
||||
return found;
|
||||
}
|
||||
|
||||
@@ -295,6 +303,8 @@ const WRITE_RELATION_OPS = /\{\s*create\b/;
|
||||
/** Does `source` call a matching operation on `model`'s own Prisma delegate? */
|
||||
function callsDelegate(source: string, model: string, ops: RegExp): boolean {
|
||||
const delegate = delegateName(model);
|
||||
// The expression below names the delegate; a file without it cannot match.
|
||||
if (!source.includes(delegate)) return false;
|
||||
for (const match of source.matchAll(
|
||||
new RegExp(`\\.${delegate}\\s*\\.\\s*(\\w+)`, "g"),
|
||||
)) {
|
||||
@@ -304,7 +314,11 @@ function callsDelegate(source: string, model: string, ops: RegExp): boolean {
|
||||
}
|
||||
|
||||
/** Every model a relation field of this name points at, across the schema. */
|
||||
const targetsCache = new Map<string, Set<string>>();
|
||||
|
||||
function targetsOfRelationField(field: string): Set<string> {
|
||||
const cached = targetsCache.get(field);
|
||||
if (cached) return cached;
|
||||
const targets = new Set<string>();
|
||||
for (const [, body] of modelBodies()) {
|
||||
for (const line of body.split("\n")) {
|
||||
@@ -312,6 +326,7 @@ function targetsOfRelationField(field: string): Set<string> {
|
||||
if (match && match[1] === field) targets.add(match[2]);
|
||||
}
|
||||
}
|
||||
targetsCache.set(field, targets);
|
||||
return targets;
|
||||
}
|
||||
|
||||
@@ -337,6 +352,7 @@ function touches(
|
||||
const source = flat(file);
|
||||
if (callsDelegate(source, model, delegateOps)) return true;
|
||||
return relationParents(model).some(({ field, parent }) => {
|
||||
if (!source.includes(field)) return false;
|
||||
if (
|
||||
!new RegExp(`\\b${field}\\s*:\\s*(?:${relationOps.source})`).test(
|
||||
source,
|
||||
|
||||
@@ -13,10 +13,9 @@
|
||||
* for that review.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -33,7 +32,7 @@ function sourceFiles(): string[] {
|
||||
}
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
return readSourceText(join(SRC, rel));
|
||||
}
|
||||
|
||||
function filesMatching(re: RegExp): string[] {
|
||||
|
||||
@@ -28,10 +28,9 @@
|
||||
* outside `app/api/`); a server-side `take` is not a fetch.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -178,7 +177,7 @@ function findLimitHits(file: string, source: string): Hit[] {
|
||||
|
||||
function allHits(): Hit[] {
|
||||
return clientFiles().flatMap((file) =>
|
||||
findLimitHits(file, readFileSync(join(SRC, file), "utf8")),
|
||||
findLimitHits(file, readSourceText(join(SRC, file))),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,10 @@
|
||||
* loaded chunk may still carry zod.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { existsSync } from "node:fs";
|
||||
import { dirname, join, normalize } from "node:path";
|
||||
import ts from "typescript";
|
||||
import { readSourceText } from "./helpers/source-files";
|
||||
|
||||
const ROOT = join(__dirname, "..", "..");
|
||||
|
||||
@@ -90,7 +91,7 @@ function clientZodImports(entry: string): { file: string; path: string[] }[] {
|
||||
const [file, inClient] = stack.pop()!;
|
||||
const prior = seen.get(file);
|
||||
if (prior === true || (prior === false && !inClient)) continue;
|
||||
const source = readFileSync(file, "utf8");
|
||||
const source = readSourceText(file);
|
||||
const client = inClient || isClient(source);
|
||||
seen.set(file, client);
|
||||
walked += 1;
|
||||
@@ -141,7 +142,7 @@ describe("client routes without the schema library", () => {
|
||||
// The schema module is zod by definition; walked as a client entry it
|
||||
// must be reported, or the walk above proves nothing.
|
||||
const probe = join(ROOT, "src/lib/validations/inbound-documents.ts");
|
||||
const source = readFileSync(probe, "utf8");
|
||||
const source = readSourceText(probe);
|
||||
expect(staticImports(source, probe).some((s) => s.startsWith("zod"))).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -33,13 +33,16 @@
|
||||
* from a variable that happens to hold a literal is not seen; review catches
|
||||
* that.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import ts from "typescript";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { stripComments, walkSourceFiles } from "./helpers/source-files";
|
||||
import {
|
||||
readSourceText,
|
||||
stripComments,
|
||||
walkSourceFiles,
|
||||
} from "./helpers/source-files";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
|
||||
@@ -67,7 +70,7 @@ function clientFiles(): ClientFile[] {
|
||||
const rel = `${dir}/${file}`;
|
||||
if (rel.startsWith("src/app/api/")) continue;
|
||||
if (rel.includes("/__tests__/") || /\.test\.tsx?$/.test(rel)) continue;
|
||||
const source = readFileSync(join(ROOT, rel), "utf8");
|
||||
const source = readSourceText(join(ROOT, rel));
|
||||
if (!isClientModule(rel, source)) continue;
|
||||
out.push({ rel, source });
|
||||
}
|
||||
@@ -75,14 +78,23 @@ function clientFiles(): ClientFile[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
const parsed = new Map<string, ts.SourceFile>();
|
||||
|
||||
/** One parse per file and source, however many rules read it. */
|
||||
function parse(name: string, source: string): ts.SourceFile {
|
||||
return ts.createSourceFile(
|
||||
name,
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
name.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
||||
);
|
||||
const key = `${name}\0${source}`;
|
||||
let file = parsed.get(key);
|
||||
if (file === undefined) {
|
||||
file = ts.createSourceFile(
|
||||
name,
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
name.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
||||
);
|
||||
parsed.set(key, file);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
/** The literal text a JSX attribute initialiser carries, if it is one. */
|
||||
@@ -218,6 +230,11 @@ function resolverImports(name: string, source: string): string[] {
|
||||
interface Rule {
|
||||
name: string;
|
||||
find: (name: string, source: string) => string[];
|
||||
/**
|
||||
* Text a file must contain for `find` to have anything to report, tested
|
||||
* before any parse. Every shape the rule matches carries one of these.
|
||||
*/
|
||||
markers: readonly string[];
|
||||
/** Sources that must produce at least one hit, proving the matcher. */
|
||||
samples: Array<{ name: string; source: string }>;
|
||||
/** File → exact hit count it may have, with the reason. */
|
||||
@@ -228,6 +245,7 @@ const RULES: Rule[] = [
|
||||
{
|
||||
name: "a literal directionSentiment",
|
||||
find: literalDirections,
|
||||
markers: ["directionSentiment"],
|
||||
samples: [
|
||||
{
|
||||
name: "sample.tsx",
|
||||
@@ -249,6 +267,7 @@ const RULES: Rule[] = [
|
||||
{
|
||||
name: "a reference line at a literal",
|
||||
find: literalLines,
|
||||
markers: [...LINE_TAGS],
|
||||
samples: [
|
||||
{
|
||||
name: "sample.tsx",
|
||||
@@ -280,12 +299,14 @@ const RULES: Rule[] = [
|
||||
{
|
||||
name: "a polarity table",
|
||||
find: polarityTables,
|
||||
markers: ["up-good", "up-bad", "UP_GOOD_", "UP_BAD_", "Sentiment"],
|
||||
samples: POLARITY_SAMPLES.map((source) => ({ name: "sample.ts", source })),
|
||||
allow: {},
|
||||
},
|
||||
{
|
||||
name: "a server resolver imported into the browser",
|
||||
find: resolverImports,
|
||||
markers: SERVER_RESOLVERS,
|
||||
samples: [
|
||||
{
|
||||
name: "sample.ts",
|
||||
@@ -299,12 +320,19 @@ const RULES: Rule[] = [
|
||||
|
||||
const FILES = clientFiles();
|
||||
|
||||
const ruleHits = new Map<Rule, Map<string, string[]>>();
|
||||
|
||||
/** Each rule's hits across the client tree, found once per run. */
|
||||
function hitsFor(rule: Rule): Map<string, string[]> {
|
||||
const out = new Map<string, string[]>();
|
||||
let out = ruleHits.get(rule);
|
||||
if (out !== undefined) return out;
|
||||
out = new Map<string, string[]>();
|
||||
for (const { rel, source } of FILES) {
|
||||
if (!rule.markers.some((marker) => source.includes(marker))) continue;
|
||||
const hits = rule.find(rel, source);
|
||||
if (hits.length > 0) out.set(rel, hits);
|
||||
}
|
||||
ruleHits.set(rule, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -321,6 +349,12 @@ describe("client threshold literal guard", () => {
|
||||
rule.find(sample.name, sample.source).length,
|
||||
`no hit on: ${sample.source}`,
|
||||
).toBeGreaterThan(0);
|
||||
// The pre-filter has to let every sample through, or it would hide
|
||||
// the shape before the matcher saw it.
|
||||
expect(
|
||||
rule.markers.some((marker) => sample.source.includes(marker)),
|
||||
`the markers skip: ${sample.source}`,
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -35,10 +35,9 @@
|
||||
* allowlist. Both the walk and every matcher therefore assert a floor.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const SRC = join(ROOT, "src");
|
||||
@@ -151,7 +150,7 @@ function sourceFiles(): string[] {
|
||||
}
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
return readSourceText(join(SRC, rel));
|
||||
}
|
||||
|
||||
/** Every environment variable shipped server code reads, however it reads it. */
|
||||
@@ -175,7 +174,7 @@ function runtimeReads(): Set<string> {
|
||||
* repeating it.
|
||||
*/
|
||||
function composeWhitelist(): Set<string> {
|
||||
const compose = readFileSync(join(ROOT, "docker-compose.yml"), "utf8");
|
||||
const compose = readSourceText(join(ROOT, "docker-compose.yml"));
|
||||
const keys = new Set<string>();
|
||||
for (const line of compose.split("\n")) {
|
||||
const m = line.match(/^\s{2,}([A-Z][A-Z0-9_]*):\s/);
|
||||
|
||||
@@ -17,11 +17,14 @@
|
||||
* inline under another name slips it. Comments are stripped first; the
|
||||
* planted cases prove it fires.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { stripComments, walkSourceFiles } from "./helpers/source-files";
|
||||
import {
|
||||
readSourceText,
|
||||
stripComments,
|
||||
walkSourceFiles,
|
||||
} from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -56,14 +59,8 @@ describe("one Pearson correlation", () => {
|
||||
});
|
||||
|
||||
it("both homes still hold their function", () => {
|
||||
const engine = readFileSync(
|
||||
join(SRC, "lib/insights/correlations.ts"),
|
||||
"utf8",
|
||||
);
|
||||
const wrapper = readFileSync(
|
||||
join(SRC, "lib/analytics/correlations.ts"),
|
||||
"utf8",
|
||||
);
|
||||
const engine = readSourceText(join(SRC, "lib/insights/correlations.ts"));
|
||||
const wrapper = readSourceText(join(SRC, "lib/analytics/correlations.ts"));
|
||||
expect(engine).toMatch(/export function pearson\s*\(/);
|
||||
expect(wrapper).toMatch(
|
||||
/export function significantPearsonCorrelation\s*\(/,
|
||||
@@ -73,14 +70,12 @@ describe("one Pearson correlation", () => {
|
||||
it("no other file defines a Pearson coefficient or calls the deleted one", () => {
|
||||
// The homes match the definition matcher, so a matcher gone blind fails here.
|
||||
const definers = sourceFiles().filter((file) =>
|
||||
PEARSON_DEFINITION.test(
|
||||
stripComments(readFileSync(join(SRC, file), "utf8")),
|
||||
),
|
||||
PEARSON_DEFINITION.test(stripComments(readSourceText(join(SRC, file)))),
|
||||
);
|
||||
expect(definers.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const offenders = sourceFiles().filter((file) => {
|
||||
const code = stripComments(readFileSync(join(SRC, file), "utf8"));
|
||||
const code = stripComments(readSourceText(join(SRC, file)));
|
||||
if (DELETED.test(code)) return true;
|
||||
return !HOMES.has(file) && PEARSON_DEFINITION.test(code);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* nothing in the client tree may derive the verdict, AND the surfaces that
|
||||
* show it must still be reading the published field.
|
||||
*/
|
||||
import { readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { readdirSync, statSync } from "node:fs";
|
||||
import { join, relative as relativeTo } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
@@ -21,8 +21,10 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
accessPaths,
|
||||
derivationFindings,
|
||||
mayBeCycleContext,
|
||||
parse,
|
||||
} from "./helpers/cycle-verdict-derivation";
|
||||
import { readSourceText } from "./helpers/source-files";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
|
||||
@@ -61,7 +63,7 @@ function clientFiles(): string[] {
|
||||
}
|
||||
|
||||
function parseFile(relative: string) {
|
||||
return parse(relative, readFileSync(join(ROOT, relative), "utf8"));
|
||||
return parse(relative, readSourceText(join(ROOT, relative)));
|
||||
}
|
||||
|
||||
describe("the cycle verdict is decided in one place", () => {
|
||||
@@ -70,9 +72,11 @@ describe("the cycle verdict is decided in one place", () => {
|
||||
// A guard over an empty file list proves nothing.
|
||||
expect(files.length).toBeGreaterThan(100);
|
||||
|
||||
const findings = files.flatMap((relative) =>
|
||||
derivationFindings(parseFile(relative)),
|
||||
);
|
||||
const findings = files
|
||||
.filter((relative) =>
|
||||
mayBeCycleContext(readSourceText(join(ROOT, relative))),
|
||||
)
|
||||
.flatMap((relative) => derivationFindings(parseFile(relative)));
|
||||
|
||||
expect(
|
||||
findings.map((f) => `${f.file}:${f.line} ${f.kind} — ${f.where}`),
|
||||
@@ -83,7 +87,7 @@ describe("the cycle verdict is decided in one place", () => {
|
||||
// The specific constant that was copied. Named here so its return is loud
|
||||
// rather than merely structural.
|
||||
const offenders = clientFiles().filter((relative) =>
|
||||
readFileSync(join(ROOT, relative), "utf8").includes("OVERDUE_GRACE_DAYS"),
|
||||
readSourceText(join(ROOT, relative)).includes("OVERDUE_GRACE_DAYS"),
|
||||
);
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
@@ -106,9 +110,8 @@ describe("the surfaces that show the verdict actually read it", () => {
|
||||
// The BBT chart used to walk the calendar itself to find the cycle start,
|
||||
// which was a second copy of the same walk. It now takes the resolved
|
||||
// value as a prop, and the cycle page feeds it from the verdict.
|
||||
const chart = readFileSync(
|
||||
const chart = readSourceText(
|
||||
join(ROOT, "src/components/cycle/bbt-chart.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
expect(chart).toContain("cycleStartDate");
|
||||
|
||||
@@ -119,10 +122,7 @@ describe("the surfaces that show the verdict actually read it", () => {
|
||||
it("the calendar response type declares the verdict as always present", () => {
|
||||
// An optional verdict is a plausible default that lets a client fall back
|
||||
// to deriving its own.
|
||||
const types = readFileSync(
|
||||
join(ROOT, "src/components/cycle/types.ts"),
|
||||
"utf8",
|
||||
);
|
||||
const types = readSourceText(join(ROOT, "src/components/cycle/types.ts"));
|
||||
expect(types).toContain("verdict: CycleVerdict;");
|
||||
expect(types).not.toContain("verdict?:");
|
||||
});
|
||||
|
||||
@@ -32,10 +32,9 @@
|
||||
* assertion. Both fixes were then restored and the guard re-ran green.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -91,7 +90,7 @@ function sourceFiles(): string[] {
|
||||
}
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
return readSourceText(join(SRC, rel));
|
||||
}
|
||||
|
||||
function count(s: string, re: RegExp): number {
|
||||
|
||||
@@ -43,10 +43,9 @@
|
||||
* how `bearer-scope-enforcement-guard` stayed green over a file it never saw.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -69,7 +68,7 @@ function sourceFiles(): string[] {
|
||||
}
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
return readSourceText(join(SRC, rel));
|
||||
}
|
||||
|
||||
function filesMatching(re: RegExp): string[] {
|
||||
|
||||
@@ -23,10 +23,9 @@
|
||||
* are pinned to a named allowlist, so a new converted-value surface
|
||||
* cannot quietly join them.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
@@ -83,7 +82,7 @@ function sourceFiles(): string[] {
|
||||
}
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
return readSourceText(join(SRC, rel));
|
||||
}
|
||||
|
||||
describe("display-transform rounding guard — the registry property", () => {
|
||||
|
||||
@@ -40,12 +40,11 @@
|
||||
* and asserts the sweep goes red without it, so every entry is shown to be
|
||||
* load-bearing, and the sweep is shown to find what it claims to find.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import ts from "typescript";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
const SCOPE = "lib/jobs/";
|
||||
@@ -257,9 +256,7 @@ const ALLOWED: Readonly<Record<string, { count: number; reason: string }>> = {
|
||||
function sweep(): Finding[] {
|
||||
return walkSourceFiles(join(SRC, SCOPE), { floor: 100 })
|
||||
.filter((p) => !p.includes("__tests__") && !p.endsWith(".test.ts"))
|
||||
.flatMap((p) =>
|
||||
scan(`${SCOPE}${p}`, readFileSync(join(SRC, SCOPE, p), "utf8")),
|
||||
);
|
||||
.flatMap((p) => scan(`${SCOPE}${p}`, readSourceText(join(SRC, SCOPE, p))));
|
||||
}
|
||||
|
||||
function unallowed(
|
||||
|
||||
@@ -22,10 +22,9 @@
|
||||
* addition defeats them, and no test substitutes for that review.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
const VALIDATOR_MODULE = "@/lib/validations/external-id";
|
||||
@@ -39,7 +38,7 @@ function sourceFiles(): string[] {
|
||||
}
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
return readSourceText(join(SRC, rel));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -377,9 +376,8 @@ describe("the intake idempotency key is never read across users", () => {
|
||||
});
|
||||
|
||||
it("the schema keeps the key unique per user, not globally", () => {
|
||||
const schema = readFileSync(
|
||||
const schema = readSourceText(
|
||||
join(process.cwd(), "prisma", "schema.prisma"),
|
||||
"utf8",
|
||||
);
|
||||
expect(schema).toContain("@@unique([userId, idempotencyKey])");
|
||||
expect(schema).not.toMatch(
|
||||
|
||||
@@ -51,7 +51,7 @@ const DAY_COUNT_NAME = /day.?of.?cycle|cycleday|cyclerun|runlength/i;
|
||||
* calendar's types, its phase names, or the verdict's own fields, so nothing
|
||||
* that could actually commit the defect escapes by renaming one identifier.
|
||||
*/
|
||||
const CYCLE_MARKERS = new Set([
|
||||
export const CYCLE_MARKERS: ReadonlySet<string> = new Set([
|
||||
"CalendarDay",
|
||||
"CalendarResponse",
|
||||
"CyclePhase",
|
||||
@@ -247,6 +247,16 @@ function isSumLike(
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a file's text could speak the cycle's vocabulary at all: every
|
||||
* marker the parse below looks for is also a substring of the text, so a
|
||||
* file holding none of them needs no parse.
|
||||
*/
|
||||
export function mayBeCycleContext(text: string): boolean {
|
||||
for (const marker of CYCLE_MARKERS) if (text.includes(marker)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether the file speaks the cycle's vocabulary at all. */
|
||||
export function isCycleContext(file: ts.SourceFile): boolean {
|
||||
let found = false;
|
||||
|
||||
@@ -305,11 +305,19 @@ function suffixBaseMatches(base: string, ref: Reference): boolean {
|
||||
|
||||
export function isReferenced(leaf: string, ref: Reference): boolean {
|
||||
if (ref.literalKeys.has(leaf)) return true;
|
||||
for (const ck of ref.literalKeys) {
|
||||
if (leaf.startsWith(`${ck}.`)) return true;
|
||||
}
|
||||
for (const base of ref.dottedBases) {
|
||||
if (leaf === base || leaf.startsWith(`${base}.`)) return true;
|
||||
// A literal key or a dotted base covers `leaf` when it IS one of the
|
||||
// leaf's dotted ancestors (or, for a base, the leaf itself), so the leaf's
|
||||
// own ancestors are looked up rather than every key in the sets scanned.
|
||||
if (ref.dottedBases.has(leaf)) return true;
|
||||
for (
|
||||
let dot = leaf.indexOf(".");
|
||||
dot !== -1;
|
||||
dot = leaf.indexOf(".", dot + 1)
|
||||
) {
|
||||
const ancestor = leaf.slice(0, dot);
|
||||
if (ref.literalKeys.has(ancestor) || ref.dottedBases.has(ancestor)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const pre of ref.concatPrefixes) {
|
||||
if (leaf.startsWith(pre)) return true;
|
||||
|
||||
@@ -51,17 +51,114 @@
|
||||
* match. Every caller roots at `src/`, `e2e/` or a subtree of one.
|
||||
*/
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { join, sep } from "node:path";
|
||||
import { join, relative, resolve, sep } from "node:path";
|
||||
|
||||
// ── The shared source index ────────────────────────────────────────────────
|
||||
//
|
||||
// The guards walk and read the same tree, and the guards project shares one
|
||||
// module graph per worker (`isolate: false`), so every walk and every read
|
||||
// below is memoised for the life of the worker: the tree is listed once per
|
||||
// top-level folder and each file is read once, however many guards ask. A
|
||||
// guard that walked `src/` and read every file again for each pattern it
|
||||
// tried ran for fifteen seconds; the same questions over the index take a
|
||||
// fraction of one.
|
||||
//
|
||||
// Only folders of the repository that do not change while tests run are
|
||||
// indexed (`INDEXED_ROOTS`). A walk or a read anywhere else (a temporary
|
||||
// fixture tree a test writes and rewrites) goes to the disk every time.
|
||||
|
||||
const INDEXED_ROOTS = ["src", "e2e", "prisma"].map((dir) =>
|
||||
resolve(process.cwd(), dir),
|
||||
);
|
||||
|
||||
function indexedRootOf(path: string): string | null {
|
||||
const absolute = resolve(path);
|
||||
return (
|
||||
INDEXED_ROOTS.find(
|
||||
(root) => absolute === root || absolute.startsWith(`${root}${sep}`),
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
const listingCache = new Map<string, string[]>();
|
||||
|
||||
/** Every entry under `root`, posix-separated, sorted; memoised when indexed. */
|
||||
function listTree(root: string): string[] {
|
||||
const absolute = resolve(root);
|
||||
const anchor = indexedRootOf(absolute);
|
||||
if (anchor === null) {
|
||||
return readdirSync(absolute, { recursive: true })
|
||||
.map((entry) => String(entry).split(sep).join("/"))
|
||||
.sort();
|
||||
}
|
||||
let all = listingCache.get(anchor);
|
||||
if (all === undefined) {
|
||||
all = readdirSync(anchor, { recursive: true })
|
||||
.map((entry) => String(entry).split(sep).join("/"))
|
||||
.sort();
|
||||
listingCache.set(anchor, all);
|
||||
}
|
||||
if (absolute === anchor) return all;
|
||||
const prefix = `${relative(anchor, absolute).split(sep).join("/")}/`;
|
||||
return all
|
||||
.filter((rel) => rel.startsWith(prefix))
|
||||
.map((rel) => rel.slice(prefix.length));
|
||||
}
|
||||
|
||||
const textCache = new Map<string, string>();
|
||||
|
||||
/**
|
||||
* A repository file's text, read once per worker when it lies in an indexed
|
||||
* folder. Use it in place of `readFileSync(path, "utf8")` for source the
|
||||
* guard reads and never writes.
|
||||
*/
|
||||
export function readSourceText(path: string): string {
|
||||
const absolute = resolve(path);
|
||||
if (indexedRootOf(absolute) === null) return readFileSync(absolute, "utf8");
|
||||
let text = textCache.get(absolute);
|
||||
if (text === undefined) {
|
||||
text = readFileSync(absolute, "utf8");
|
||||
textCache.set(absolute, text);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* The files under `root` (as `walkSourceFiles` lists them) whose text holds
|
||||
* at least one of `markers`, with that text. The substring test runs before
|
||||
* any regular expression or parse the caller applies, which is where most of
|
||||
* a sweep's time used to go. The floor applies to the files walked, as in
|
||||
* `walkSourceFiles`; a caller that expects matches states its own floor over
|
||||
* the result.
|
||||
*/
|
||||
export function sourcesContaining(
|
||||
root: string,
|
||||
markers: string | readonly string[],
|
||||
options: {
|
||||
floor: number;
|
||||
extensions?: readonly string[];
|
||||
include?: (rel: string) => boolean;
|
||||
},
|
||||
): Array<[rel: string, text: string]> {
|
||||
const wanted = typeof markers === "string" ? [markers] : markers;
|
||||
const out: Array<[string, string]> = [];
|
||||
for (const rel of walkSourceFiles(root, options).filter(
|
||||
options.include ?? (() => true),
|
||||
)) {
|
||||
const text = readSourceText(join(root, rel));
|
||||
if (wanted.some((marker) => text.includes(marker))) out.push([rel, text]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function walkSourceFiles(
|
||||
root: string,
|
||||
options: { floor: number; extensions?: readonly string[] },
|
||||
): string[] {
|
||||
const extensions = options.extensions ?? [".ts", ".tsx"];
|
||||
const files = readdirSync(root, { recursive: true })
|
||||
.map((entry) => String(entry).split(sep).join("/"))
|
||||
.filter((rel) => extensions.some((ext) => rel.endsWith(ext)))
|
||||
.sort();
|
||||
const files = listTree(root).filter((rel) =>
|
||||
extensions.some((ext) => rel.endsWith(ext)),
|
||||
);
|
||||
|
||||
if (files.length < options.floor) {
|
||||
throw new Error(
|
||||
@@ -216,7 +313,7 @@ export function scanSourceMatches(
|
||||
|
||||
const matches: SourceMatch[] = [];
|
||||
for (const file of files) {
|
||||
const raw = readFileSync(join(root, file), "utf8");
|
||||
const raw = readSourceText(join(root, file));
|
||||
const source = options.stripComments ? stripComments(raw) : raw;
|
||||
for (const m of source.matchAll(pattern)) {
|
||||
const line = source.slice(0, m.index).split("\n").length;
|
||||
@@ -273,7 +370,7 @@ export function readSource(
|
||||
`readSourceFolder and a floor.`,
|
||||
);
|
||||
}
|
||||
const text = readFileSync(path, "utf8");
|
||||
const text = readSourceText(path);
|
||||
const lost = list.filter((anchor) => anchorCount(text, anchor) === 0);
|
||||
if (lost.length > 0) {
|
||||
throw new Error(
|
||||
@@ -306,5 +403,5 @@ export function readSourceFolder(
|
||||
extensions: options.extensions,
|
||||
}).filter(options.include ?? (() => true));
|
||||
requireFloor(`readSourceFolder(${relDir})`, files, options.floor);
|
||||
return files.map((rel) => [rel, readFileSync(join(root, rel), "utf8")]);
|
||||
return files.map((rel) => [rel, readSourceText(join(root, rel))]);
|
||||
}
|
||||
@@ -26,10 +26,11 @@
|
||||
* its keys report as orphans.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { readdirSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { collectReferences, isReferenced } from "./helpers/i18n-reference";
|
||||
import { readSourceText } from "./helpers/source-files";
|
||||
|
||||
const ROOT = join(__dirname, "../..");
|
||||
const SRC = join(ROOT, "src");
|
||||
@@ -142,8 +143,16 @@ function isAllowlisted(leaf: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
let scanned: ReturnType<typeof scanOnce> | null = null;
|
||||
|
||||
/** The bundle and the tree's references, worked out once per run. */
|
||||
function scan() {
|
||||
const en = JSON.parse(readFileSync(EN_BUNDLE_PATH, "utf8"));
|
||||
scanned ??= scanOnce();
|
||||
return scanned;
|
||||
}
|
||||
|
||||
function scanOnce() {
|
||||
const en = JSON.parse(readSourceText(EN_BUNDLE_PATH));
|
||||
const leaves: string[] = [];
|
||||
leafPaths(en, "", leaves);
|
||||
|
||||
@@ -152,7 +161,7 @@ function scan() {
|
||||
const topNamespaces = new Set<string>(Object.keys(en));
|
||||
|
||||
const allText = walk(SRC)
|
||||
.map((f) => readFileSync(f, "utf8"))
|
||||
.map((f) => readSourceText(f))
|
||||
.join("\n");
|
||||
|
||||
return { leaves, ...collectReferences(allText, allNodes, topNamespaces) };
|
||||
|
||||
@@ -19,12 +19,13 @@
|
||||
* hand-listed: the expected set is read out of the route tree on every run, so
|
||||
* the guard cannot fall behind the code it watches.
|
||||
*/
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { readdirSync } from "node:fs";
|
||||
import { join, relative, sep } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildOpenApiDocument } from "@/lib/openapi/registry";
|
||||
import { readSourceText } from "./helpers/source-files";
|
||||
|
||||
const API_ROOT = join(process.cwd(), "src", "app", "api");
|
||||
|
||||
@@ -74,7 +75,7 @@ function idempotentOperationsFromSource(): Set<string> {
|
||||
const pattern =
|
||||
/export\s+const\s+(POST|PUT|PATCH|DELETE)\s*=\s*([\s\S]*?)(?=\n(?:export\s+const|export\s+async\s+function|$))/g;
|
||||
for (const file of routeFiles(API_ROOT)) {
|
||||
const source = readFileSync(file, "utf8");
|
||||
const source = readSourceText(file);
|
||||
if (!source.includes("withIdempotency")) continue;
|
||||
for (const match of source.matchAll(pattern)) {
|
||||
if (!/\bwithIdempotency\s*[(<]/.test(match[2])) continue;
|
||||
|
||||
@@ -12,12 +12,15 @@
|
||||
* The readable name survives only as the snapshot builder's input field, which
|
||||
* carries the OPENED text, never the column.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { stripComments, walkSourceFiles } from "./helpers/source-files";
|
||||
import {
|
||||
readSourceText,
|
||||
stripComments,
|
||||
walkSourceFiles,
|
||||
} from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -45,9 +48,7 @@ function sourceFilesMatching(pattern: RegExp): string[] {
|
||||
return walkSourceFiles(SRC, { floor: 2000 })
|
||||
.filter((f) => !f.startsWith("generated/"))
|
||||
.filter((f) => !f.includes("__tests__/") && !/\.test\.tsx?$/.test(f))
|
||||
.filter((f) =>
|
||||
pattern.test(stripComments(readFileSync(join(SRC, f), "utf8"))),
|
||||
)
|
||||
.filter((f) => pattern.test(stripComments(readSourceText(join(SRC, f)))))
|
||||
.sort();
|
||||
}
|
||||
|
||||
|
||||
@@ -23,11 +23,14 @@
|
||||
* Comments are stripped before matching, and the planted cases prove each
|
||||
* matcher finds what it claims to.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { stripComments, walkSourceFiles } from "./helpers/source-files";
|
||||
import {
|
||||
readSourceText,
|
||||
stripComments,
|
||||
walkSourceFiles,
|
||||
} from "./helpers/source-files";
|
||||
|
||||
/** The shared comment strip, plus SQL line comments inside template strings. */
|
||||
function stripCode(text: string): string {
|
||||
@@ -98,7 +101,7 @@ describe("latest-per-type reads", () => {
|
||||
|
||||
it("no production read keeps one row per type with DISTINCT ON", () => {
|
||||
const offenders = productionFiles().filter((file) =>
|
||||
LATEST_PER_TYPE.test(stripCode(readFileSync(join(SRC, file), "utf8"))),
|
||||
LATEST_PER_TYPE.test(stripCode(readSourceText(join(SRC, file)))),
|
||||
);
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
@@ -129,9 +132,7 @@ describe("latest-per-type reads", () => {
|
||||
it("no production read groups every row for the latest per type", () => {
|
||||
const files = productionFiles();
|
||||
const grouped = files.filter((file) =>
|
||||
/\.measurement\s*\.\s*groupBy\s*\(/.test(
|
||||
readFileSync(join(SRC, file), "utf8"),
|
||||
),
|
||||
/\.measurement\s*\.\s*groupBy\s*\(/.test(readSourceText(join(SRC, file))),
|
||||
);
|
||||
// The tree groups measurements in a dozen places; finding none means
|
||||
// the walk read the wrong tree.
|
||||
@@ -139,16 +140,16 @@ describe("latest-per-type reads", () => {
|
||||
const offenders = files
|
||||
.filter((file) => !(file in GROUPBY_ALLOWED))
|
||||
.flatMap((file) =>
|
||||
latestOnlyGroupBys(
|
||||
stripComments(readFileSync(join(SRC, file), "utf8")),
|
||||
).map((arg) => `${file}: ${arg.slice(0, 100)}`),
|
||||
latestOnlyGroupBys(stripComments(readSourceText(join(SRC, file)))).map(
|
||||
(arg) => `${file}: ${arg.slice(0, 100)}`,
|
||||
),
|
||||
);
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
|
||||
it("the summaries slice is built only behind its cache cell", () => {
|
||||
const callers = productionFiles().filter((file) =>
|
||||
SLICE_BUILDER_CALL.test(stripCode(readFileSync(join(SRC, file), "utf8"))),
|
||||
SLICE_BUILDER_CALL.test(stripCode(readSourceText(join(SRC, file)))),
|
||||
);
|
||||
expect(callers).toEqual(["lib/analytics/summaries-slice.ts"]);
|
||||
});
|
||||
|
||||
@@ -7,11 +7,10 @@
|
||||
* implementation. Comments are stripped first; the planted cases prove the
|
||||
* matcher fires.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -52,25 +51,21 @@ describe("one least-squares line", () => {
|
||||
const offenders = files.filter(
|
||||
(file) =>
|
||||
!(file in ALLOWED) &&
|
||||
HAND_ROLLED_SLOPE.test(
|
||||
stripComments(readFileSync(join(SRC, file), "utf8")),
|
||||
),
|
||||
HAND_ROLLED_SLOPE.test(stripComments(readSourceText(join(SRC, file)))),
|
||||
);
|
||||
expect(offenders).toEqual([]);
|
||||
// The six readers now call the implementation: a sweep that finds fewer
|
||||
// call sites read the wrong tree or lost one.
|
||||
const callers = files.filter((file) =>
|
||||
/\b(?:linearFit|fitFromSums|trendLineByTime)\s*\(/.test(
|
||||
stripComments(readFileSync(join(SRC, file), "utf8")),
|
||||
stripComments(readSourceText(join(SRC, file))),
|
||||
),
|
||||
);
|
||||
expect(callers.length).toBeGreaterThanOrEqual(7);
|
||||
// The implementation itself matches, so the sweep read real files.
|
||||
expect(
|
||||
HAND_ROLLED_SLOPE.test(
|
||||
stripComments(
|
||||
readFileSync(join(SRC, "lib/stats/linear-fit.ts"), "utf8"),
|
||||
),
|
||||
stripComments(readSourceText(join(SRC, "lib/stats/linear-fit.ts"))),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -19,10 +19,9 @@
|
||||
* test substitutes for that review.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -164,14 +163,25 @@ function sourceFiles(): string[] {
|
||||
}
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
return readSourceText(join(SRC, rel));
|
||||
}
|
||||
|
||||
let writers: string[] | null = null;
|
||||
|
||||
/**
|
||||
* Every file that writes a measurement, found once per run. A write names
|
||||
* the `measurement` delegate or the bulk insert's `MeasurementRows`, so a
|
||||
* file holding neither word is skipped before the expressions run.
|
||||
*/
|
||||
function discoveredWriters(): string[] {
|
||||
return sourceFiles().filter((rel) => {
|
||||
writers ??= sourceFiles().filter((rel) => {
|
||||
const text = read(rel);
|
||||
if (!text.includes("measurement") && !text.includes("MeasurementRows")) {
|
||||
return false;
|
||||
}
|
||||
return MEASUREMENT_WRITE.test(text) || BULK_INSERT_CALL.test(text);
|
||||
});
|
||||
return writers;
|
||||
}
|
||||
|
||||
describe("every measurement writer declares how it meets the plausibility domain", () => {
|
||||
@@ -195,9 +205,10 @@ describe("every measurement writer declares how it meets the plausibility domain
|
||||
});
|
||||
|
||||
it("raw SQL inserts measurements in the bulk-insert module and nowhere else", () => {
|
||||
const raw = sourceFiles().filter((rel) =>
|
||||
RAW_MEASUREMENT_INSERT.test(read(rel)),
|
||||
);
|
||||
const raw = sourceFiles().filter((rel) => {
|
||||
const text = read(rel);
|
||||
return /insert/i.test(text) && RAW_MEASUREMENT_INSERT.test(text);
|
||||
});
|
||||
expect(raw).toEqual([BULK_INSERT_MODULE]);
|
||||
});
|
||||
|
||||
|
||||
@@ -18,18 +18,17 @@
|
||||
*
|
||||
* Every check is also run against a source broken on purpose.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
const MEMORY = "lib/ai/coach/memory";
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
return readSourceText(join(SRC, rel));
|
||||
}
|
||||
|
||||
function sourceFiles(): string[] {
|
||||
|
||||
@@ -17,11 +17,14 @@
|
||||
* (`bucket_start`) is out of its reach by name. Comments are stripped first;
|
||||
* the planted cases prove it fires. It is a tripwire, not a proof.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { stripComments, walkSourceFiles } from "./helpers/source-files";
|
||||
import {
|
||||
readSourceText,
|
||||
stripComments,
|
||||
walkSourceFiles,
|
||||
} from "./helpers/source-files";
|
||||
|
||||
/** The shared comment strip, plus SQL line comments inside template strings. */
|
||||
function stripCode(text: string): string {
|
||||
@@ -59,8 +62,7 @@ describe("naive timestamp zone shifts", () => {
|
||||
!file.includes("__tests__/") &&
|
||||
!/\.test\.tsx?$/.test(file),
|
||||
);
|
||||
const code = (file: string) =>
|
||||
stripCode(readFileSync(join(SRC, file), "utf8"));
|
||||
const code = (file: string) => stripCode(readSourceText(join(SRC, file)));
|
||||
// The reads that shift correctly, so a sweep that reads nothing fails.
|
||||
const doubleShifts = files.filter((file) => DOUBLE_SHIFT.test(code(file)));
|
||||
expect(doubleShifts.length).toBeGreaterThanOrEqual(5);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { walkSourceFiles } from "@/__tests__/helpers/source-files";
|
||||
import { readSourceText } from "./helpers/source-files";
|
||||
|
||||
/**
|
||||
* No raw control byte in a source file, apart from tab, newline and carriage
|
||||
@@ -28,7 +28,7 @@ describe("source files carry no raw control bytes", () => {
|
||||
expect(files.length).toBeGreaterThan(3000);
|
||||
const offenders: string[] = [];
|
||||
for (const file of files) {
|
||||
const lines = readFileSync(join(src, file), "utf8").split("\n");
|
||||
const lines = readSourceText(join(src, file)).split("\n");
|
||||
lines.forEach((line, i) => {
|
||||
if (CONTROL.test(line)) offenders.push(`${file}:${i + 1}`);
|
||||
});
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
* that stops matching fails instead of passing.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -54,7 +54,7 @@ function productionFiles(): string[] {
|
||||
}
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(process.cwd(), rel), "utf8");
|
||||
return readSourceText(join(process.cwd(), rel));
|
||||
}
|
||||
|
||||
describe("OAuth token lifecycle guard", () => {
|
||||
|
||||
@@ -20,10 +20,9 @@
|
||||
* frozen so that adding one has to be a deliberate edit here.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -47,7 +46,7 @@ function sourceFiles(): string[] {
|
||||
}
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
return readSourceText(join(SRC, rel));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,15 +14,14 @@
|
||||
* with every list.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
return readSourceText(join(SRC, rel));
|
||||
}
|
||||
|
||||
function sourceFiles(): string[] {
|
||||
|
||||
@@ -39,10 +39,9 @@
|
||||
* shipped several of those.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -55,7 +54,7 @@ function sourceFiles(): string[] {
|
||||
}
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
return readSourceText(join(SRC, rel));
|
||||
}
|
||||
|
||||
function filesMatching(re: RegExp): string[] {
|
||||
|
||||
@@ -19,10 +19,11 @@
|
||||
* `RecurrenceContext` annotation is invisible here. Every medication path
|
||||
* today goes through one of the annotated builders below.
|
||||
*/
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { readdirSync } from "node:fs";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readSourceText } from "./helpers/source-files";
|
||||
|
||||
const SRC = resolve(__dirname, "..");
|
||||
const ROOT = resolve(SRC, "..");
|
||||
@@ -73,7 +74,7 @@ interface Builder {
|
||||
function builders(): Builder[] {
|
||||
const out: Builder[] = [];
|
||||
for (const file of sourceFiles(SRC)) {
|
||||
const text = readFileSync(file, "utf8");
|
||||
const text = readSourceText(file);
|
||||
for (const m of text.matchAll(DECLARATION)) {
|
||||
const open = m.index! + m[0].length - 1;
|
||||
out.push({
|
||||
|
||||
@@ -33,10 +33,9 @@
|
||||
* matches nothing fails instead of passing.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -55,7 +54,7 @@ function appFiles(): string[] {
|
||||
}
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
return readSourceText(join(SRC, rel));
|
||||
}
|
||||
|
||||
function appFilesMatching(re: RegExp): string[] {
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
* different set of symbols and answers it from the AST, because a route
|
||||
* explaining in a comment why it is NOT delegable is a thing worth writing.
|
||||
*/
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { readdirSync } from "node:fs";
|
||||
import { join, sep } from "node:path";
|
||||
|
||||
import ts from "typescript";
|
||||
@@ -75,6 +75,7 @@ import { ENTIRE_RECORD, SHARE_DOMAINS } from "@/lib/sharing/scope";
|
||||
import type { ShareScope } from "@/lib/sharing/scope";
|
||||
|
||||
import { ADMITTED_MUTATING_HANDLERS } from "../../tests/fixtures/v137/sharing-matrix";
|
||||
import { readSourceText } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -198,7 +199,7 @@ const textCache = new Map<string, string>();
|
||||
function read(rel: string): string {
|
||||
const cached = textCache.get(rel);
|
||||
if (cached !== undefined) return cached;
|
||||
const text = readFileSync(join(SRC, rel), "utf8");
|
||||
const text = readSourceText(join(SRC, rel));
|
||||
textCache.set(rel, text);
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -16,11 +16,14 @@
|
||||
* `glucoseSD`) and is out of the matcher's reach by shape. Comments are
|
||||
* stripped first; the planted case proves the matcher fires.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { stripComments, walkSourceFiles } from "./helpers/source-files";
|
||||
import {
|
||||
readSourceText,
|
||||
stripComments,
|
||||
walkSourceFiles,
|
||||
} from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -51,7 +54,7 @@ function computesSpread(code: string): boolean {
|
||||
}
|
||||
|
||||
function read(file: string): string {
|
||||
return stripComments(readFileSync(join(SRC, file), "utf8"));
|
||||
return stripComments(readSourceText(join(SRC, file)));
|
||||
}
|
||||
|
||||
describe("one standard deviation", () => {
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
* The few bare reads left are listed below with the reason they are safe:
|
||||
* each feeds a `timestamptz` column or returns the instant to JavaScript.
|
||||
*/
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { readdirSync, statSync } from "node:fs";
|
||||
import { join, relative } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readSourceText } from "./helpers/source-files";
|
||||
|
||||
const ROOT = join(__dirname, "..", "..");
|
||||
const SRC = join(ROOT, "src");
|
||||
@@ -69,7 +70,7 @@ function bareReads(): string[] {
|
||||
const found: string[] = [];
|
||||
for (const file of walk(SRC)) {
|
||||
const rel = relative(ROOT, file);
|
||||
readFileSync(file, "utf8")
|
||||
readSourceText(file)
|
||||
.split("\n")
|
||||
.forEach((line, index) => {
|
||||
if (/^\s*(\/\/|\*|\/\*|--)/.test(line)) return;
|
||||
@@ -84,7 +85,7 @@ function bareReads(): string[] {
|
||||
describe("raw SQL clock reads are anchored to UTC", () => {
|
||||
it("finds the anchored form across the swept files", () => {
|
||||
// The matcher must see something, or a broken walk would pass.
|
||||
const passkey = readFileSync(join(SRC, "lib/auth/passkey.ts"), "utf8");
|
||||
const passkey = readSourceText(join(SRC, "lib/auth/passkey.ts"));
|
||||
expect(passkey.match(ANCHORED)?.length).toBeGreaterThanOrEqual(3);
|
||||
expect(SQL_CLOCK.test("AND expires_at > NOW()")).toBe(true);
|
||||
expect(SQL_CLOCK.test("updated_at = now()")).toBe(true);
|
||||
|
||||
@@ -23,10 +23,9 @@
|
||||
* only that it has not changed without someone editing this file.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -39,7 +38,7 @@ function sourceFiles(): string[] {
|
||||
}
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
return readSourceText(join(SRC, rel));
|
||||
}
|
||||
|
||||
/** Local names bound for `exportName` from any module matching `moduleRe`. */
|
||||
|
||||
@@ -32,27 +32,33 @@
|
||||
* with every list.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
return readSourceText(join(SRC, rel));
|
||||
}
|
||||
|
||||
/**
|
||||
* Source with comments removed, so a route that only DESCRIBES a gate in its
|
||||
* doc comment is not counted, and a path quoted in a comment is not a call.
|
||||
*/
|
||||
const codeCache = new Map<string, string>();
|
||||
|
||||
function code(rel: string): string {
|
||||
return read(rel)
|
||||
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||
.split("\n")
|
||||
.map((line) => line.replace(/(^|[^:"'`\\])\/\/.*$/, "$1"))
|
||||
.join("\n");
|
||||
let stripped = codeCache.get(rel);
|
||||
if (stripped === undefined) {
|
||||
stripped = read(rel)
|
||||
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||
.split("\n")
|
||||
.map((line) => line.replace(/(^|[^:"'`\\])\/\/.*$/, "$1"))
|
||||
.join("\n");
|
||||
codeCache.set(rel, stripped);
|
||||
}
|
||||
return stripped;
|
||||
}
|
||||
|
||||
const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"] as const;
|
||||
@@ -252,7 +258,15 @@ function hookCallersWrap(
|
||||
return callers > 0;
|
||||
}
|
||||
|
||||
let gatedCalls: GatedCall[] | null = null;
|
||||
|
||||
/** Every web call of a gated route, found once per run. */
|
||||
function gatedWebCalls(): GatedCall[] {
|
||||
gatedCalls ??= findGatedWebCalls();
|
||||
return gatedCalls;
|
||||
}
|
||||
|
||||
function findGatedWebCalls(): GatedCall[] {
|
||||
const calls: GatedCall[] = [];
|
||||
const files = webFiles();
|
||||
const sources = new Map(files.map((f) => [f, code(f)]));
|
||||
@@ -260,8 +274,12 @@ function gatedWebCalls(): GatedCall[] {
|
||||
if (GATED_ROUTES[route] !== "web") continue;
|
||||
const [method, path] = route.split(" ");
|
||||
const re = pathPattern(path);
|
||||
// Every match spells the path's static head out literally, up to the
|
||||
// first `[param]`; a file without it cannot match and is not searched.
|
||||
const head = path.split("/[")[0];
|
||||
for (const file of files) {
|
||||
const src = sources.get(file) ?? "";
|
||||
if (!src.includes(head)) continue;
|
||||
for (const m of src.matchAll(re)) {
|
||||
const index = m.index ?? 0;
|
||||
// The path handed to a component as a prop (`endpoint="/api/…"`):
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
/**
|
||||
* A formatter that renders an instant must say which calendar it renders in.
|
||||
*
|
||||
@@ -29,6 +30,54 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
const REPO_ROOT = resolve(__dirname, "../..");
|
||||
|
||||
/** A source file under `src/`, by its repo-relative path, and its text. */
|
||||
interface SourceFile {
|
||||
file: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
/** Whatever any scan below looks for, as plain substrings. */
|
||||
const MARKERS = [
|
||||
"Intl.DateTimeFormat",
|
||||
".toLocaleDateString",
|
||||
".toLocaleTimeString",
|
||||
"getDateTimeFormat",
|
||||
"fmt.date",
|
||||
];
|
||||
|
||||
let candidates: SourceFile[] | null = null;
|
||||
|
||||
/**
|
||||
* Every source file that mentions one of the markers, read once per run.
|
||||
*
|
||||
* Each scan used to start its own `grep -r` over the whole of `src/`,
|
||||
* generated client included, five times per run; under a loaded unit suite
|
||||
* that alone ran past the 5 s test timeout. The tree is walked once now, the
|
||||
* generated Prisma client left out (it renders no dates, and no scan has ever
|
||||
* matched in it), and each file is kept only when a plain substring test says
|
||||
* a pattern could match, before any regular expression runs.
|
||||
*/
|
||||
function candidateFiles(): SourceFile[] {
|
||||
candidates ??= walkSourceFiles(resolve(REPO_ROOT, "src"), { floor: 3000 })
|
||||
.filter((rel) => !rel.startsWith("generated/"))
|
||||
.map((rel) => ({
|
||||
file: `src/${rel}`,
|
||||
source: readFileSync(resolve(REPO_ROOT, "src", rel), "utf8"),
|
||||
}))
|
||||
.filter(({ source }) => MARKERS.some((marker) => source.includes(marker)));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/** The candidate files a pattern matches, test files left in. */
|
||||
function filesMatching(pattern: RegExp, under = "src/"): SourceFile[] {
|
||||
return candidateFiles().filter(
|
||||
({ file, source }) => file.startsWith(under) && pattern.test(source),
|
||||
);
|
||||
}
|
||||
|
||||
const ANY_FORMATTER =
|
||||
/new Intl\.DateTimeFormat|\.toLocale(Date|Time)String\(|getDateTimeFormat\(/;
|
||||
|
||||
/**
|
||||
* Bare formatter sites that are correct as they stand. Keep this short: it is
|
||||
* an inventory of exceptions, not a parking space. Every entry says why, and
|
||||
@@ -97,26 +146,12 @@ function topLevelArgCount(args: string): number {
|
||||
}
|
||||
|
||||
function bareFormatterSites(): Array<{ file: string; line: number }> {
|
||||
const listed = execFileSync(
|
||||
"grep",
|
||||
[
|
||||
"-rlE",
|
||||
"new Intl\\.DateTimeFormat|\\.toLocale(Date|Time)String\\(|getDateTimeFormat\\(",
|
||||
"src",
|
||||
"--include=*.ts",
|
||||
"--include=*.tsx",
|
||||
],
|
||||
{ cwd: REPO_ROOT, encoding: "utf8" },
|
||||
)
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
const listed = filesMatching(ANY_FORMATTER)
|
||||
// Tests pin instants on purpose and are not user-facing surfaces.
|
||||
.filter((f) => !f.includes("__tests__"));
|
||||
.filter(({ file }) => !file.includes("__tests__"));
|
||||
|
||||
const hits: Array<{ file: string; line: number }> = [];
|
||||
for (const file of listed) {
|
||||
const source = readFileSync(resolve(REPO_ROOT, file), "utf8");
|
||||
for (const { file, source } of listed) {
|
||||
for (const pattern of FORMATTER_PATTERNS) {
|
||||
pattern.lastIndex = 0;
|
||||
for (let m = pattern.exec(source); m !== null; m = pattern.exec(source)) {
|
||||
@@ -158,21 +193,7 @@ describe("date formatters name the calendar they render in", () => {
|
||||
it("finds formatter sites at all (the guard must not pass vacuously)", () => {
|
||||
// If this drops to zero the grep has stopped matching and every assertion
|
||||
// below became free.
|
||||
const all = execFileSync(
|
||||
"grep",
|
||||
[
|
||||
"-rlE",
|
||||
"new Intl\\.DateTimeFormat|\\.toLocale(Date|Time)String\\(|getDateTimeFormat\\(",
|
||||
"src",
|
||||
"--include=*.ts",
|
||||
"--include=*.tsx",
|
||||
],
|
||||
{ cwd: REPO_ROOT, encoding: "utf8" },
|
||||
)
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean);
|
||||
expect(all.length).toBeGreaterThan(5);
|
||||
expect(filesMatching(ANY_FORMATTER).length).toBeGreaterThan(5);
|
||||
});
|
||||
|
||||
it("has no unexplained formatter without an explicit timeZone", () => {
|
||||
@@ -241,26 +262,17 @@ describe("date formatters name the calendar they render in", () => {
|
||||
describe("components format dates through useFormatters", () => {
|
||||
const CALL = /\bgetDateTimeFormat\s*\(/g;
|
||||
|
||||
let scanned: { offenders: string[]; calls: number } | null = null;
|
||||
|
||||
function scan() {
|
||||
const files = execFileSync(
|
||||
"grep",
|
||||
[
|
||||
"-rlE",
|
||||
"getDateTimeFormat\\s*\\(",
|
||||
"src/components",
|
||||
"--include=*.ts",
|
||||
"--include=*.tsx",
|
||||
],
|
||||
{ cwd: REPO_ROOT, encoding: "utf8" },
|
||||
)
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.filter((f) => !f.includes("__tests__"));
|
||||
if (scanned) return scanned;
|
||||
const files = filesMatching(
|
||||
/getDateTimeFormat\s*\(/,
|
||||
"src/components/",
|
||||
).filter(({ file }) => !file.includes("__tests__"));
|
||||
const offenders: string[] = [];
|
||||
let calls = 0;
|
||||
for (const file of files) {
|
||||
const source = readFileSync(resolve(REPO_ROOT, file), "utf8");
|
||||
for (const { file, source } of files) {
|
||||
CALL.lastIndex = 0;
|
||||
for (let m = CALL.exec(source); m; m = CALL.exec(source)) {
|
||||
const lineStart = source.lastIndexOf("\n", m.index) + 1;
|
||||
@@ -275,7 +287,8 @@ describe("components format dates through useFormatters", () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
return { offenders, calls };
|
||||
scanned = { offenders, calls };
|
||||
return scanned;
|
||||
}
|
||||
|
||||
it("still sees the cache's calls in components (the matcher matches)", () => {
|
||||
@@ -317,20 +330,16 @@ describe("a stated date is formatted on its calendar date", () => {
|
||||
const CALENDAR_CALL = /\bfmt\.dateShortSmartCalendar\s*\(/g;
|
||||
const NOON_ANCHOR = /T12:00:00/;
|
||||
|
||||
let scanned: { offenders: string[]; calendarCalls: number } | null = null;
|
||||
|
||||
function scan() {
|
||||
const files = execFileSync(
|
||||
"grep",
|
||||
["-rlE", "fmt\\.date", "src", "--include=*.ts", "--include=*.tsx"],
|
||||
{ cwd: REPO_ROOT, encoding: "utf8" },
|
||||
)
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.filter((f) => !f.includes("__tests__"));
|
||||
if (scanned) return scanned;
|
||||
const files = filesMatching(/fmt\.date/).filter(
|
||||
({ file }) => !file.includes("__tests__"),
|
||||
);
|
||||
const offenders: string[] = [];
|
||||
let calendarCalls = 0;
|
||||
for (const file of files) {
|
||||
const source = readFileSync(resolve(REPO_ROOT, file), "utf8");
|
||||
for (const { file, source } of files) {
|
||||
calendarCalls += (source.match(CALENDAR_CALL) ?? []).length;
|
||||
if (/const fmt = makeBucketLabelFormatters\(/.test(source)) continue;
|
||||
ZONED_CALL.lastIndex = 0;
|
||||
@@ -343,7 +352,8 @@ describe("a stated date is formatted on its calendar date", () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
return { offenders, calendarCalls };
|
||||
scanned = { offenders, calendarCalls };
|
||||
return scanned;
|
||||
}
|
||||
|
||||
it("finds the calendar formatter in use (the matcher still matches)", () => {
|
||||
|
||||
@@ -18,10 +18,9 @@
|
||||
* settles is that there is only one of it.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -81,7 +80,7 @@ function sourceFiles(): string[] {
|
||||
}
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
return readSourceText(join(SRC, rel));
|
||||
}
|
||||
|
||||
/** A line that declares `relativeDueKey`, ignoring comment prose. */
|
||||
|
||||
@@ -19,10 +19,9 @@
|
||||
* that a new ingest cannot appear without someone editing this file.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -36,7 +35,7 @@ function sourceFiles(): string[] {
|
||||
}
|
||||
|
||||
function read(rel: string): string {
|
||||
return readFileSync(join(SRC, rel), "utf8");
|
||||
return readSourceText(join(SRC, rel));
|
||||
}
|
||||
|
||||
interface ProviderIngest {
|
||||
|
||||
Reference in new issue
Block a user