Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -1108,6 +1108,50 @@
"document": "readonly"
}
},
{
"files": ["scripts/**/*.test.ts", "vscode/src/**/*.{ts,tsx}"],
"rules": {
"no-unused-vars": [
"error",
{
"vars": "all",
"args": "after-used",
"ignoreRestSiblings": true,
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrors": "all"
}
]
},
"globals": {
"console": "readonly",
"process": "readonly",
"Buffer": "readonly",
"__dirname": "readonly",
"__filename": "readonly",
"require": "readonly",
"setTimeout": "readonly",
"clearTimeout": "readonly",
"setInterval": "readonly",
"clearInterval": "readonly",
"window": "readonly",
"document": "readonly"
}
},
{
"files": ["vscode/src/**/*.tsx"],
"plugins": ["react"],
"rules": {
"react/rules-of-hooks": "error",
"react/exhaustive-deps": "warn"
}
},
{
"files": ["scripts/**/*.test.ts", "vscode/src/**/*.test.{ts,tsx}"],
"rules": {
"no-empty-function": "off"
}
},
{
"files": ["**/*.test.ts", "**/*.test.tsx"],
"globals": {
Expand Down
10 changes: 7 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -626,17 +626,21 @@ check-bench-agent: node_modules/.installed src/version.ts $(BUILTIN_SKILLS_GENER
@./scripts/check-bench-agent.sh

LINT_PREREQS := node_modules/.installed src/version.ts $(BUILTIN_SKILLS_GENERATED) $(BUILTIN_WORKFLOWS_GENERATED) $(WORKFLOW_RUNTIME_SOURCES_GENERATED)
# Type-aware ESLint lints vscode/src, whose types (@types/vscode) come from the extension's own
# install. Without it, `make -j static-check-full` races typecheck's install and every vscode
# type reads as `error`.
ESLINT_PREREQS := $(LINT_PREREQS) vscode/node_modules/.installed

lint: $(LINT_PREREQS) ## Run oxlint on all files (the eslint.config.mjs rules it supports; no type-aware rules)
@./scripts/lint.sh

lint-eslint: $(LINT_PREREQS) ## Run oxlint, then type-aware ESLint on files changed since the merge base with origin/main [XUM_LINT_BASE=<ref>]
lint-eslint: $(ESLINT_PREREQS) ## Run oxlint, then type-aware ESLint on files changed since the merge base with origin/main [XUM_LINT_BASE=<ref>]
@./scripts/lint.sh --eslint

lint-full: $(LINT_PREREQS) ## Run oxlint, then ESLint on all files, one ESLint run per machine at a time (as CI does)
lint-full: $(ESLINT_PREREQS) ## Run oxlint, then ESLint on all files, one ESLint run per machine at a time (as CI does)
@./scripts/lint.sh --full

lint-fix: $(LINT_PREREQS) ## Run oxlint --fix, then ESLint --fix on changed files (scripts/lint.sh --full --fix for all files)
lint-fix: $(ESLINT_PREREQS) ## Run oxlint --fix, then ESLint --fix on changed files (scripts/lint.sh --full --fix for all files)
@./scripts/lint.sh --eslint --fix

lint-actions: lint-actionlint lint-zizmor ## Lint GitHub Actions workflows
Expand Down
60 changes: 60 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,66 @@ export default defineConfig([
],
},
},
{
// Tooling tests (scripts/**/*.test.ts) and the VS Code extension (vscode/src) are type-checked
// by tsconfig.tooling.json (make typecheck), not by the tsconfig.json the project service
// would pick, so lint them against that program with the same base rules as tests/.
files: ["scripts/**/*.test.ts", "vscode/src/**/*.{ts,tsx}"],
plugins: {
local: localPlugin,
"react-hooks": reactHooks,
},
languageOptions: {
parserOptions: {
project: "./tsconfig.tooling.json",
tsconfigRootDir: import.meta.dirname,
},
globals: {
console: "readonly",
process: "readonly",
Buffer: "readonly",
__dirname: "readonly",
__filename: "readonly",
require: "readonly",
setTimeout: "readonly",
clearTimeout: "readonly",
setInterval: "readonly",
clearInterval: "readonly",
window: "readonly",
document: "readonly",
},
},
rules: {
// Same options as src/ and tests/: a leading underscore marks an intentionally unused binding.
"@typescript-eslint/no-unused-vars": [
"error",
{
vars: "all",
args: "after-used",
ignoreRestSiblings: true,
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrors: "all",
},
],
},
},
{
// The webview is React: run the two hook rules src/ runs (src/ also runs the compiler rules).
files: ["vscode/src/**/*.tsx"],
rules: {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
},
},
{
// Like tests/: test doubles implement async interfaces with synchronous or empty bodies.
files: ["scripts/**/*.test.ts", "vscode/src/**/*.test.{ts,tsx}"],
rules: {
"@typescript-eslint/require-await": "off",
"@typescript-eslint/no-empty-function": "off",
},
},
{
// Test file configuration
files: ["**/*.test.ts", "**/*.test.tsx"],
Expand Down
1 change: 1 addition & 0 deletions scripts/check-startup-imports-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/await-thenable -- bun:test types `await expect(...).rejects.toThrow()` as void */
// Fixture tests for scripts/check-startup-imports-runtime.ts. Each test writes a tiny
// project to a temp dir and loads its main.js through the real Node harness.
import { afterEach, beforeEach, expect, test } from "bun:test";
Expand Down
2 changes: 1 addition & 1 deletion scripts/checkOxlintConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ async function mismatches(
files,
oxlintConfig,
eslintOptions: { overrideConfigFile: true, overrideConfig: eslintConfig },
})) as Map<string, string[]>;
}));
return Object.fromEntries(found);
} finally {
fs.rmSync(cwd, { recursive: true, force: true });
Expand Down
23 changes: 10 additions & 13 deletions scripts/dry/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { spawnSync } from "node:child_process";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import type { DryReport } from "./check";

const CHECK = join(import.meta.dir, "check.ts");
// Hermetic git: CI has no identity, and host config (hooks, signing) must not leak in.
Expand All @@ -27,13 +28,15 @@ function write(path: string, text: string): void {
writeFileSync(join(repo, path), text);
}

function check() {
function check(): { status: number | null; report: DryReport } {
const result = spawnSync(process.execPath, [CHECK, "--base", "HEAD", "--json"], {
cwd: repo,
env: GIT_ENV,
encoding: "utf8",
});
return { status: result.status, report: result.status === 2 ? null : JSON.parse(result.stdout) };
// Exit 2 is a usage or tool error with no JSON report; no test expects it.
if (result.status === 2) throw new Error(`check.ts exited 2: ${result.stderr}`);
return { status: result.status, report: JSON.parse(result.stdout) as DryReport };
}

const bucket = (name: string) => `export function ${name}(n: number): string {
Expand Down Expand Up @@ -63,9 +66,7 @@ test("a new copy of existing code is a finding", () => {
const { status, report } = check();

expect(status).toBe(1);
const pairs = (report.pairs as Array<{ left: { name: string }; right: { name: string } }>).map(
(pair) => `${pair.left.name}~${pair.right.name}`
);
const pairs = report.pairs.map((pair) => `${pair.left.name}~${pair.right.name}`);
expect(pairs.sort()).toEqual(["fee~cents", "fee~count"]);
});

Expand Down Expand Up @@ -113,8 +114,7 @@ test("a new copy that shares a name with an old match is a finding", () => {
const { status, report } = check();

expect(status).toBe(1);
type Side = { path: string; name: string; startLine: number };
const pairs = (report.pairs as Array<{ left: Side; right: Side }>)
const pairs = report.pairs
.filter((p) => p.left.path === "src/r.ts" && p.left.startLine === 2)
.filter((p) => p.right.path === "src/r.ts")
.map((p) => p.right.name);
Expand All @@ -134,7 +134,7 @@ test("a dry-duplicate-ok marker in the changed function accepts the pair", () =>
const { status, report } = check();

expect(status).toBe(0);
const reasons = (report.pairs as Array<{ accepted?: string }>).map((p) => p.accepted);
const reasons = report.pairs.map((p) => p.accepted);
expect(reasons).toEqual(["fees and cents must change apart", "fees and cents must change apart"]);
});

Expand All @@ -148,7 +148,7 @@ test("a marker on the old side does not excuse a new copy", () => {
const { status, report } = check();

expect(status).toBe(1);
expect((report.pairs as Array<{ accepted?: string }>).some((p) => p.accepted)).toBe(false);
expect(report.pairs.some((p) => p.accepted)).toBe(false);
});

test("a function moved to another file is reported again (moves are not tracked)", () => {
Expand All @@ -172,9 +172,6 @@ test("a function moved to another file is reported again (moves are not tracked)

// cents~count matched before the move. See new_pairs in dryer_report.py.
expect(status).toBe(1);
type Side = { name: string };
const pairs = (report.pairs as Array<{ left: Side; right: Side }>).map(
(pair) => `${pair.left.name}~${pair.right.name}`
);
const pairs = report.pairs.map((pair) => `${pair.left.name}~${pair.right.name}`);
expect(pairs).toEqual(["cents~count"]);
});
1 change: 1 addition & 0 deletions scripts/generate-npm-shrinkwrap.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/await-thenable -- bun:test types `await expect(...).rejects.toThrow()` as void */
import { describe, expect, test } from "bun:test";
import {
buildShrinkwrap,
Expand Down
15 changes: 13 additions & 2 deletions scripts/lib/lint_targets.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const LINT_FILE_PATTERNS = [
/^src\/node\/builtinSkills\/.+\.js$/,
/^src\/node\/workflowRuntime\/[^/]+\.js$/,
/^scripts\/lib\/[^/]+\.js$/,
/^scripts\/.+\.test\.ts$/,
/^vscode\/src\/.+\.tsx?$/,
Comment thread
ThomasK33 marked this conversation as resolved.
];

// Changes that can alter findings in files the branch did not edit, so only a full run is
Expand All @@ -32,6 +34,10 @@ const FULL_LINT_TRIGGERS = [
/^eslint-local-rules\.mjs$/,
/^package\.json$/,
/^bun\.lock$/,
// vscode/src is linted with the extension's own types (@types/vscode), so an update to its
// dependencies can surface findings in unchanged files.
/^vscode\/package\.json$/,
/^vscode\/bun\.lock$/,
/^tsconfig[^/]*\.json$/,
/^scripts\/lint\.sh$/,
/^scripts\/lib\/lint_targets\.js$/,
Expand Down Expand Up @@ -109,13 +115,18 @@ function listChangedFiles(cwd, base) {
* oxlint walks these directories itself: a list of every file would exceed the Windows
* command-line limit (~32K characters). Every file that LINT_FILE_PATTERNS matches is inside one.
*/
const OXLINT_ROOTS = ["src", "tests", "scripts/lib"];
const OXLINT_ROOTS = ["src", "tests", "scripts", "vscode/src"];
Comment thread
ThomasK33 marked this conversation as resolved.
// The extensions oxlint lints when it walks a directory.
const OXLINT_FILE_EXTENSIONS = /\.(?:[cm]?[jt]sx?|vue|svelte|astro)$/;

function listFiles(cwd, args) {
return (
git(["ls-files", "-z", ...args, "--", ...OXLINT_ROOTS], cwd)
// Exclude node_modules in the query itself: an installed scripts/mintlify/node_modules
// lists megabytes of ignored paths, more than execFileSync's output buffer holds.
git(
["ls-files", "-z", ...args, "--", ...OXLINT_ROOTS, ":(exclude,glob)**/node_modules/**"],
cwd
)
.split("\0")
.filter((file) => file !== "")
// ESLint never lints node_modules.
Expand Down
2 changes: 2 additions & 0 deletions scripts/lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ ESLINT_PATTERNS=(
'src/node/builtinSkills/**/*.js'
'src/node/workflowRuntime/*.js'
'scripts/lib/*.js'
'scripts/**/*.test.ts'
'vscode/src/**/*.{ts,tsx}'
)

get_default_eslint_concurrency() {
Expand Down
5 changes: 4 additions & 1 deletion scripts/mutate/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ function check(): { status: number | null; report: MutateReport | null } {
env: GIT_ENV,
encoding: "utf8",
});
return { status: result.status, report: result.status === 2 ? null : JSON.parse(result.stdout) };
return {
status: result.status,
report: result.status === 2 ? null : (JSON.parse(result.stdout) as MutateReport),
};
}

function outcomes(report: MutateReport | null): string[] {
Expand Down
2 changes: 1 addition & 1 deletion scripts/notarize-mac.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ exit 99
process.env.PATH = `${binDir}${path.delimiter}${savedEnv.PATH ?? ""}`;
}

function calls(): Array<{ command: string; cwd: string }> {
function calls(): { command: string; cwd: string }[] {
if (!fs.existsSync(callLog)) return [];
return fs
.readFileSync(callLog, "utf8")
Expand Down
14 changes: 8 additions & 6 deletions scripts/orpcSchemasBarrelLint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ async function restrictedImports(filePath: string, code: string): Promise<string
return messages.map((message) => message.message);
}

const barrelMessage = expect.stringContaining("not the @/common/orpc/schemas barrel");
const nodeMessage = expect.stringContaining("must not value-import src/node");
const backendOnlyMessage = expect.stringContaining("module is backend-only");
const shikiMessage = expect.stringContaining("Shiki must only be imported in highlightWorker.ts");
const tokenizerMessage = expect.stringContaining("ai-tokenizer/encoding barrel");
const barrelMessage = expect.stringContaining("not the @/common/orpc/schemas barrel") as string;
const nodeMessage = expect.stringContaining("must not value-import src/node") as string;
const backendOnlyMessage = expect.stringContaining("module is backend-only") as string;
const shikiMessage = expect.stringContaining(
"Shiki must only be imported in highlightWorker.ts"
) as string;
const tokenizerMessage = expect.stringContaining("ai-tokenizer/encoding barrel") as string;

describe("orpc schemas barrel guard", () => {
test.each([
Expand Down Expand Up @@ -166,7 +168,7 @@ describe("src/node guard for browser and common code", () => {
[
"src/common/config/schemas/example.test.ts",
'import { X } from "@/common/orpc/schemas/runtime";',
expect.stringContaining("Config schemas must not import from ORPC"),
expect.stringContaining("Config schemas must not import from ORPC") as string,
],
])("keeps the folder rules for tests in %s: %s", async (filePath, code, message) => {
expect(await restrictedImports(filePath, code)).toEqual([message]);
Expand Down
18 changes: 12 additions & 6 deletions scripts/perf/analyzeProfilesCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ describe("leaderboard", () => {
});
// A and B run in an unmapped .js bundle; a leaderboard warns too, not only a diff.
expect(report.warnings).toEqual([
expect.stringContaining("1.1 ms of self time in unmapped bundle frames"),
expect.stringContaining("1.1 ms of self time in unmapped bundle frames") as string,
]);
});

Expand Down Expand Up @@ -157,7 +157,11 @@ describe("readCpuProfile", () => {
)
);
expect(result.profile.stacks).toEqual([
{ frames: [expect.objectContaining({ functionName: "A" })], weightUs: 500, count: 3 },
{
frames: [expect.objectContaining({ functionName: "A" }) as Frame],
weightUs: 500,
count: 3,
},
]);
expect(result.warnings).toEqual(["1 negative timeDeltas clamped to 0 (sample reordering)"]);
});
Expand All @@ -171,7 +175,7 @@ describe("readCpuProfile", () => {
[2],
[10]
);
(json.nodes as Array<Record<string, unknown>>)[1].parent = 1;
(json.nodes as Record<string, unknown>[])[1].parent = 1;
expect(read(json).profile.stacks[0].frames.map((f) => f.functionName)).toEqual(["A"]);
});

Expand Down Expand Up @@ -243,7 +247,7 @@ describe("frame category", () => {

describe("diff", () => {
/** One profile with a flat list of functions and one sample each; wall time in µs. */
function flat(wallUs: number, functions: Array<[string, number]>): Record<string, unknown> {
function flat(wallUs: number, functions: [string, number][]): Record<string, unknown> {
return cpuProfile(
[
{ id: 1, name: "(root)", children: functions.map((_, i) => i + 2) },
Expand Down Expand Up @@ -784,7 +788,7 @@ test("CLI maps each diff side with its own maps when bundle names are stable", (
["cand.ts"],
]);
expect((JSON.parse(unmatched.stdout) as Report).warnings).toContainEqual(
expect.stringContaining("--baseline-map-dir matched no profiled script")
expect.stringContaining("--baseline-map-dir matched no profiled script") as string
);
// Without --baseline there is no baseline side for the option to map.
const noBaseline = runCli([
Expand Down Expand Up @@ -813,7 +817,9 @@ test("folded stacks keep scripts that share a file name apart", () => {
.trimEnd()
.split("\n");
expect(lines).toHaveLength(2);
expect(lines.map((l) => Number(l.split(" ").pop()))).toEqual(expect.arrayContaining([1, 2]));
expect(lines.map((l) => Number(l.split(" ").pop()))).toEqual(
expect.arrayContaining([1, 2]) as number[]
);
});

test("mapped frames with one name on one original line stay apart by original column", () => {
Expand Down
Loading
Loading