Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0bd6b02
[lint-diff] Add eslint deps
mikeharder Nov 20, 2025
8b4d0a5
npm i
mikeharder Nov 20, 2025
e0c3288
eslint minimal config
mikeharder Nov 20, 2025
26f4b07
enable lint in action
mikeharder Nov 20, 2025
0274a0b
[runChecks.ts] let -> const
mikeharder Nov 20, 2025
9e6811f
[correlateResults.ts] any -> unknown
mikeharder Nov 20, 2025
d46c104
[lintdiff-types.ts] any -> unknown
mikeharder Nov 20, 2025
93d0e5d
[markdown-utils] any -> unknown, string interpolation
mikeharder Nov 20, 2025
3bf7514
enable typeChecked and test/**
mikeharder Nov 21, 2025
f199e03
[generateReport.ts] toString()
mikeharder Nov 21, 2025
6eec00b
[lint-diff.ts] inspect
mikeharder Nov 21, 2025
a399373
[markdown-utils.ts] Downcast tokens
mikeharder Nov 21, 2025
7ece8b9
[processChanges.ts] Remove unnecessary async/await
mikeharder Nov 21, 2025
97d7c17
[runChecks.ts] remove unnecessary downcast
mikeharder Nov 21, 2025
5127216
[correlateResults.test.ts] unnecessary async
mikeharder Nov 21, 2025
8dae980
[generateReport.test.ts] cast -> generic
mikeharder Nov 21, 2025
c2121ab
[processChanges.fs.test.ts] cast -> generic
mikeharder Nov 21, 2025
3457e29
[processChanges.test.ts] async/await
mikeharder Nov 21, 2025
fbfe8b0
[runChecks.test.ts] await, any
mikeharder Nov 21, 2025
294a338
[util.test.ts] simplify memfs mocking
mikeharder Nov 21, 2025
571dacc
[runChecks.test.ts] as -> decl
mikeharder Nov 21, 2025
492efb8
[processChanges.test.ts] Fix expect
mikeharder Nov 21, 2025
12ff95e
[correlateResults.ts] toString(), cast JSON.parse()
mikeharder Nov 21, 2025
cfa3e5f
[util.ts] cast JSON.parse()
mikeharder Nov 21, 2025
1600c74
Merge branch 'main' into lint-diff-eslint-minimal
mikeharder Nov 21, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/lintdiff-test.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
name: Swagger LintDiff - Test

on:
Expand Down Expand Up @@ -31,4 +31,4 @@
uses: ./.github/workflows/_reusable-eng-tools-test.yaml
with:
package: lint-diff
lint: false
lint: true
30 changes: 30 additions & 0 deletions eng/tools/lint-diff/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Keep in sync with .github/shared/eslint.config.js

import eslint from "@eslint/js";
import { defineConfig } from "eslint/config";
import globals from "globals";
import tseslint from "typescript-eslint";

/** @type {import('eslint').Linter.Config[]} */
export default defineConfig(
eslint.configs.recommended,
tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
// we only run in node, not browser
globals: globals.node,
// required to use tseslint.configs.recommendedTypeChecked
parserOptions: {
projectService: {
allowDefaultProject: ["*.js", "cmd/*.js"],
},
// ensures the tsconfig path resolves relative to this file
// default is process.cwd() when running eslint, which may be incorrect
tsconfigRootDir: import.meta.dirname,
},
},
},
{
ignores: ["coverage/**", "dist/**"],
},
);
5 changes: 5 additions & 0 deletions eng/tools/lint-diff/package.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"name": "@azure-tools/lint-diff",
"private": true,
Expand All @@ -8,10 +8,12 @@
},
"scripts": {
"build": "tsc --build",
"check": "npm run build && npm run lint && npm run format:check && npm run test:ci",
"clean": "rm -rf dist && rm -rf node_modules",
"format": "prettier . --ignore-path ../.prettierignore --write",
"format:check": "prettier . --ignore-path ../.prettierignore --check",
"format:check:ci": "prettier . --ignore-path ../.prettierignore --check --log-level debug",
"lint": "cross-env DEBUG=eslint:eslint eslint",
"test": "vitest",
"test:ci": "vitest run --coverage --reporter=verbose"
},
Expand All @@ -30,14 +32,17 @@
"marked": "^17.0.0"
},
"devDependencies": {
"@eslint/js": "^9.22.0",
"@types/deep-eql": "^4.0.2",
"@types/node": "^18.19.31",
"@vitest/coverage-v8": "^3.0.2",
"eslint": "^9.22.0",
"execa": "^9.5.2",
"memfs": "^4.17.0",
"prettier": "~3.6.2",
"prettier-plugin-organize-imports": "^4.2.0",
"typescript": "~5.9.2",
"typescript-eslint": "^8.45.0",
"vitest": "^3.2.4"
}
}
10 changes: 6 additions & 4 deletions eng/tools/lint-diff/src/correlateResults.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { Readme } from "@azure-tools/specs-shared/readme";
import { basename, join, relative } from "path";
import { AutorestRunResult, BeforeAfter, LintDiffViolation, Source } from "./lintdiff-types.js";
Expand Down Expand Up @@ -40,7 +40,7 @@
const beforeReadme = new Readme(beforeReadmePath);
const defaultTag = await getDefaultTag(beforeReadme);
if (!defaultTag) {
throw new Error(`No default tag found for readme ${readme} in before state`);
throw new Error(`No default tag found for readme ${readme.toString()} in before state`);
}
const beforeDefaultTagCandidates = beforeChecks.filter(
(r) => relative(beforePath, r.readme.path) === readmePathRelative && r.tag === defaultTag,
Expand Down Expand Up @@ -69,7 +69,9 @@
});
continue;
} else if (beforeReadmeCandidate.length > 1) {
throw new Error(`Multiple before candidates found for key ${key} using readme ${readme}`);
throw new Error(
`Multiple before candidates found for key ${key} using readme ${readme.toString()}`,
);
}
}

Expand Down Expand Up @@ -148,7 +150,7 @@
continue;
}

const result = JSON.parse(line.trim());
const result = JSON.parse(line.trim()) as { code: string };
if (result.code == undefined) {
// Results without a code can be assumed to be fatal errors. Set the code
// to "FATAL"
Expand Down Expand Up @@ -213,7 +215,7 @@
return true;
}

export function arrayIsEqual(a: any[], b: any[]) {
export function arrayIsEqual(a: unknown[], b: unknown[]) {
if (a.length !== b.length) {
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion eng/tools/lint-diff/src/generateReport.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { kebabCase } from "change-case";
import { writeFile } from "node:fs/promises";
import { relative } from "node:path";
Expand Down Expand Up @@ -148,7 +148,7 @@
console.error("LintDiff detected AutoRest errors");
outputMarkdown += "**AutoRest errors:**\n\n";
for (const { result, errors } of autoRestErrors) {
console.log(`AutoRest errors for ${result.readme} (${result.tag})`);
console.log(`AutoRest errors for ${result.readme.toString()} (${result.tag})`);

const readmePath = relative(result.rootPath, result.readme.path);

Expand Down
8 changes: 4 additions & 4 deletions eng/tools/lint-diff/src/lint-diff.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SpecModelError } from "@azure-tools/specs-shared/spec-model-error";
import { writeFile } from "node:fs/promises";
import { parseArgs, ParseArgsConfig } from "node:util";
import { inspect, parseArgs, ParseArgsConfig } from "node:util";
import { correlateRuns } from "./correlateResults.js";
import { generateAutoRestErrorReport, generateLintDiffReport } from "./generateReport.js";
import { getRunList } from "./processChanges.js";
Expand Down Expand Up @@ -67,13 +67,13 @@
// TODO: Handle trailing slashes properly
if (!beforeArg || !(await pathExists(beforeArg as string))) {
validArgs = false;
console.log(`--before must be a valid path. Value passed: ${beforeArg || "<empty>"}`);
console.log(`--before must be a valid path. Value passed: ${inspect(beforeArg) || "<empty>"}`);
}

// TODO: Handle trailing slashes properly
if (!afterArg || !(await pathExists(afterArg as string))) {
validArgs = false;
console.log(`--after must be a valid path. Value passed: ${afterArg || "<empty>"}`);
console.log(`--after must be a valid path. Value passed: ${inspect(afterArg) || "<empty>"}`);
}

if (!changedFilesPath || !(await pathExists(changedFilesPath as string))) {
Expand Down Expand Up @@ -121,7 +121,7 @@
} catch (error) {
if (error instanceof SpecModelError) {
console.log("\n❌ Error building Spec Model from changed file list:");
console.log(`${error}`);
console.log(`${inspect(error)}`);

process.exitCode = 1;
return;
Expand Down
2 changes: 1 addition & 1 deletion eng/tools/lint-diff/src/lintdiff-types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { Readme } from "@azure-tools/specs-shared/readme";
import { ExecException } from "node:child_process";

Expand All @@ -14,7 +14,7 @@

export interface AutoRestMessage {
level: "information" | "warning" | "error" | "debug" | "verbose" | "fatal";
code?: any;
code?: unknown;
message: string;
readme?: string;
tag?: string;
Expand Down
15 changes: 8 additions & 7 deletions eng/tools/lint-diff/src/markdown-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Readme } from "@azure-tools/specs-shared/readme";
import { kebabCase } from "change-case";
import { marked } from "marked";
import { marked, Tokens } from "marked";
import { inspect } from "util";

export enum MarkdownType {
Arm = "arm",
Expand Down Expand Up @@ -88,9 +89,9 @@
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (e: any) {
} catch (e) {
// TODO: Retry? Fail ungracefully?
console.log(`GET ${docUrl} failed with ${e.message} .`);
console.log(`GET ${docUrl} failed with ${inspect(e)}.`);
rpcInfoCache.set(ruleName, rpcRules);
return rpcRules;
}
Expand All @@ -101,13 +102,13 @@
const token = tokens[i];
if (
token.type === "heading" &&
token.depth >= 1 &&
token.text.trim().toLowerCase() === "related arm guideline code"
(token as Tokens.Heading).depth >= 1 &&
(token as Tokens.Heading).text.trim().toLowerCase() === "related arm guideline code"
) {
// The next token should be a list
const next = tokens[i + 1];
if (next && next.type === "list" && Array.isArray(next.items)) {
for (const item of next.items) {
if (next && next.type === "list") {
for (const item of (next as Tokens.List).items) {
// item.text may contain comma-separated codes
if (typeof item.text === "string") {
rpcRules.push(...item.text.split(",").map((c: string) => c.trim()));
Expand Down
6 changes: 3 additions & 3 deletions eng/tools/lint-diff/src/processChanges.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { readme, swagger } from "@azure-tools/specs-shared/changed-files";
import { SpecModel } from "@azure-tools/specs-shared/spec-model";
import deepEqual from "deep-eql";
Expand Down Expand Up @@ -82,7 +82,7 @@

// Get affected services from changed files
// e.g. specification/service1/readme.md -> specification/service1
const affectedServiceDirectories = await getAffectedServices(existingChangedFiles);
const affectedServiceDirectories = getAffectedServices(existingChangedFiles);

// Build service models of affected services
const specModels = new Map<string, SpecModel>();
Expand Down Expand Up @@ -233,10 +233,10 @@
* @param changedFiles a list of changed files
* @returns A list of "services" that are affected by the changed files
*/
export async function getAffectedServices(changedFiles: string[]) {
export function getAffectedServices(changedFiles: string[]) {
const affectedServices = new Set<string>();
for (const file of changedFiles) {
const service = await getService(file);
const service = getService(file);
if (service) {
affectedServices.add(service);
}
Expand Down
13 changes: 6 additions & 7 deletions eng/tools/lint-diff/src/runChecks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExecError, execNpmExec, isExecError } from "@azure-tools/specs-shared/exec";
import { execNpmExec, isExecError } from "@azure-tools/specs-shared/exec";
import { debugLogger } from "@azure-tools/specs-shared/logger";
import { join } from "path";

Expand All @@ -22,14 +22,14 @@
for (const [readme, tags] of runList.entries()) {
const changedFilePath = join(path, readme);

let openApiType = await getOpenapiType(tags.readme);
const openApiType = await getOpenapiType(tags.readme);

// From momentOfTruth.ts:executeAutoRestWithLintDiff
// This is a quick workaround for https://github.com/Azure/azure-sdk-tools/issues/6549
// We override the openapi-subtype with the value of openapi-type,
// to prevent LintDiff from reading openapi-subtype from the AutoRest config file (README)
// and overriding openapi-type with it.
let openApiSubType = openApiType;
const openApiSubType = openApiType;

// If the tags array is empty run the loop once but with a null tag
const coalescedTags = tags.changedTags?.size ? [...tags.changedTags] : [null];
Expand Down Expand Up @@ -77,16 +77,15 @@
throw error;
}

const execError = error as ExecError;
lintDiffResult = {
autorestCommand,
rootPath: path,
readme: tags.readme,
tag: tag ? tag : "",
openApiType,
error: execError,
stdout: execError.stdout || "",
stderr: execError.stderr || "",
error,
stdout: error.stdout || "",
stderr: error.stderr || "",
} as AutorestRunResult;

logAutorestExecutionErrors(lintDiffResult);
Expand Down
4 changes: 3 additions & 1 deletion eng/tools/lint-diff/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ export async function pathExists(path: string): Promise<boolean> {
/* v8 ignore start */
export async function getDependencyVersion(dependenciesDir: string): Promise<string> {
const packageJsonPath = join(dependenciesDir, "package.json");
const packageJson = JSON.parse(await readFile(packageJsonPath, { encoding: "utf-8" }));
const packageJson = JSON.parse(await readFile(packageJsonPath, { encoding: "utf-8" })) as {
version?: string;
};
const version = packageJson.version;
if (!version) {
throw new Error(`Version not found in package.json at ${packageJsonPath}`);
Expand Down
12 changes: 6 additions & 6 deletions eng/tools/lint-diff/test/correlateResults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ describe("isSameSources", () => {
});
});

describe("getLintDiffViolations", async () => {
describe("getLintDiffViolations", () => {
function createRunResult(stdout: string, stderr: string = ""): AutorestRunResult {
return {
rootPath: "string",
Expand Down Expand Up @@ -359,39 +359,39 @@ describe("getLintDiffViolations", async () => {
});

describe("arrayIsEqual", () => {
test("returns true for equal arrays", async () => {
test("returns true for equal arrays", () => {
const a = ["a", "b", "c"];
const b = ["a", "b", "c"];

const result = arrayIsEqual(a, b);
expect(result).toEqual(true);
});

test("returns false for different arrays", async () => {
test("returns false for different arrays", () => {
const a = ["a", "b", "c"];
const b = ["a", "b", "d"];

const result = arrayIsEqual(a, b);
expect(result).toEqual(false);
});

test("returns false for different lengths", async () => {
test("returns false for different lengths", () => {
const a = ["a", "b", "c"];
const b = ["a", "b"];

const result = arrayIsEqual(a, b);
expect(result).toEqual(false);
});

test("returns true for empty arrays", async () => {
test("returns true for empty arrays", () => {
const a: string[] = [];
const b: string[] = [];

const result = arrayIsEqual(a, b);
expect(result).toEqual(true);
});

test("returns true for equal arrays with different types", async () => {
test("returns true for equal arrays with different types", () => {
const a = ["a", 1, "c"];
const b = ["a", 1, "c"];

Expand Down
2 changes: 1 addition & 1 deletion eng/tools/lint-diff/test/generateReport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { isWindows } from "./test-util.js";
import { vol } from "memfs";

vi.mock("node:fs/promises", async () => {
const memfs = (await vi.importActual("memfs")) as typeof import("memfs");
const memfs = await vi.importActual<typeof import("memfs")>("memfs");
return {
...memfs.fs.promises,
};
Expand Down
2 changes: 1 addition & 1 deletion eng/tools/lint-diff/test/processChanges.fs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { readFileList } from "../src/processChanges.js";
// These tests are in a separate module because fs mocking is difficult to undo

vi.mock("node:fs/promises", async () => {
const memfs = (await vi.importActual("memfs")) as typeof import("memfs");
const memfs = await vi.importActual<typeof import("memfs")>("memfs");
return {
...memfs.fs.promises,
};
Expand Down
Loading
Loading