Skip to content

Commit 5da8fe7

Browse files
Linter all rulesets automatically created (#3462)
Provide a built in way for Azure/typespec-azure#330
1 parent 90b8d6e commit 5da8fe7

File tree

10 files changed

+113
-33
lines changed

10 files changed

+113
-33
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
3+
changeKind: feature
4+
packages:
5+
- "@typespec/compiler"
6+
---
7+
8+
Linter `all` rulesets is automatically created if not explicitly provided
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
3+
changeKind: feature
4+
packages:
5+
- "@typespec/http"
6+
---
7+
8+
Use new compiler automatic `all` ruleset instead of explicitly provided one

packages/compiler/src/core/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export {
4040
setCadlNamespace,
4141
setTypeSpecNamespace,
4242
} from "./library.js";
43+
export { resolveLinterDefinition } from "./linter.js";
4344
export * from "./module-resolver.js";
4445
export { NodeHost } from "./node-host.js";
4546
export { Numeric, isNumeric } from "./numeric.js";

packages/compiler/src/core/linter.ts

+36-14
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
DiagnosticMessages,
99
LibraryInstance,
1010
LinterDefinition,
11+
LinterResolvedDefinition,
1112
LinterRule,
1213
LinterRuleContext,
1314
LinterRuleDiagnosticReport,
@@ -22,6 +23,34 @@ export interface Linter {
2223
lint(): readonly Diagnostic[];
2324
}
2425

26+
/**
27+
* Resolve the linter definition
28+
*/
29+
export function resolveLinterDefinition(
30+
libName: string,
31+
linter: LinterDefinition
32+
): LinterResolvedDefinition {
33+
const rules: LinterRule<string, any>[] = linter.rules.map((rule) => {
34+
return { ...rule, id: `${libName}/${rule.name}` };
35+
});
36+
if (linter.ruleSets && "all" in linter.ruleSets) {
37+
return {
38+
rules,
39+
ruleSets: linter.ruleSets as any,
40+
};
41+
} else {
42+
return {
43+
rules,
44+
ruleSets: {
45+
all: {
46+
enable: Object.fromEntries(rules.map((x) => [x.id, true])) as any,
47+
},
48+
...linter.ruleSets,
49+
},
50+
};
51+
}
52+
}
53+
2554
export function createLinter(
2655
program: Program,
2756
loadLibrary: (name: string) => Promise<LibraryInstance | undefined>
@@ -37,11 +66,6 @@ export function createLinter(
3766
lint,
3867
};
3968

40-
function getLinterDefinition(library: LibraryInstance): LinterDefinition | undefined {
41-
// eslint-disable-next-line deprecation/deprecation
42-
return library?.linter ?? library?.definition?.linter;
43-
}
44-
4569
async function extendRuleSet(ruleSet: LinterRuleSet): Promise<readonly Diagnostic[]> {
4670
tracer.trace("extend-rule-set.start", JSON.stringify(ruleSet, null, 2));
4771
const diagnostics = createDiagnosticCollector();
@@ -50,7 +74,7 @@ export function createLinter(
5074
const ref = diagnostics.pipe(parseRuleReference(extendingRuleSetName));
5175
if (ref) {
5276
const library = await resolveLibrary(ref.libraryName);
53-
const libLinterDefinition = library && getLinterDefinition(library);
77+
const libLinterDefinition = library?.linter;
5478
const extendingRuleSet = libLinterDefinition?.ruleSets?.[ref.name];
5579
if (extendingRuleSet) {
5680
await extendRuleSet(extendingRuleSet);
@@ -146,19 +170,17 @@ export function createLinter(
146170
tracer.trace("register-library", name);
147171

148172
const library = await loadLibrary(name);
149-
const linter = library && getLinterDefinition(library);
173+
const linter = library?.linter;
150174
if (linter?.rules) {
151-
for (const ruleDef of linter.rules) {
152-
const ruleId = `${name}/${ruleDef.name}`;
175+
for (const rule of linter.rules) {
153176
tracer.trace(
154177
"register-library.rule",
155-
`Registering rule "${ruleId}" for library "${name}".`
178+
`Registering rule "${rule.id}" for library "${name}".`
156179
);
157-
const rule: LinterRule<string, any> = { ...ruleDef, id: ruleId };
158-
if (ruleMap.has(ruleId)) {
159-
compilerAssert(false, `Unexpected duplicate linter rule: "${ruleId}"`);
180+
if (ruleMap.has(rule.id)) {
181+
compilerAssert(false, `Unexpected duplicate linter rule: "${rule.id}"`);
160182
} else {
161-
ruleMap.set(ruleId, rule);
183+
ruleMap.set(rule.id, rule);
162184
}
163185
}
164186
}

packages/compiler/src/core/program.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
} from "./entrypoint-resolution.js";
2222
import { ExternalError } from "./external-error.js";
2323
import { getLibraryUrlsLoaded } from "./library.js";
24-
import { createLinter } from "./linter.js";
24+
import { createLinter, resolveLinterDefinition } from "./linter.js";
2525
import { createLogger } from "./logger/index.js";
2626
import { createTracer } from "./logger/tracer.js";
2727
import { createDiagnostic } from "./messages.js";
@@ -580,12 +580,13 @@ export async function compile(
580580

581581
const libDefinition: TypeSpecLibrary<any> | undefined = entrypoint?.esmExports.$lib;
582582
const metadata = computeLibraryMetadata(module, libDefinition);
583-
583+
// eslint-disable-next-line deprecation/deprecation
584+
const linterDef = entrypoint?.esmExports.$linter ?? libDefinition?.linter;
584585
return {
585586
...resolution,
586587
metadata,
587588
definition: libDefinition,
588-
linter: entrypoint?.esmExports.$linter,
589+
linter: linterDef && resolveLinterDefinition(libraryNameOrPath, linterDef),
589590
};
590591
}
591592

packages/compiler/src/core/types.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -2047,7 +2047,7 @@ export interface LibraryInstance {
20472047
entrypoint: JsSourceFileNode | undefined;
20482048
metadata: LibraryMetadata;
20492049
definition?: TypeSpecLibrary<any>;
2050-
linter: LinterDefinition;
2050+
linter: LinterResolvedDefinition;
20512051
}
20522052

20532053
export type LibraryMetadata = FileLibraryMetadata | ModuleLibraryMetadata;
@@ -2437,6 +2437,14 @@ export interface LinterDefinition {
24372437
ruleSets?: Record<string, LinterRuleSet>;
24382438
}
24392439

2440+
export interface LinterResolvedDefinition {
2441+
readonly rules: LinterRule<string, DiagnosticMessages>[];
2442+
readonly ruleSets: {
2443+
all: LinterRuleSet;
2444+
[name: string]: LinterRuleSet;
2445+
};
2446+
}
2447+
24402448
export interface LinterRuleDefinition<N extends string, DM extends DiagnosticMessages> {
24412449
/** Rule name (without the library name) */
24422450
name: N;

packages/compiler/test/core/linter.test.ts

+42-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, it } from "vitest";
22

33
import { createLinterRule, createTypeSpecLibrary } from "../../src/core/library.js";
4-
import { Linter, createLinter } from "../../src/core/linter.js";
4+
import { Linter, createLinter, resolveLinterDefinition } from "../../src/core/linter.js";
55
import type { LibraryInstance, LinterDefinition } from "../../src/index.js";
66
import {
77
createTestHost,
@@ -45,13 +45,13 @@ describe("compiler: linter", () => {
4545

4646
const library: LibraryInstance = {
4747
entrypoint: undefined,
48-
metadata: { type: "module", name: "@typespec/test" },
48+
metadata: { type: "module", name: "@typespec/test-linter" },
4949
module: { type: "module", path: "", mainFile: "", manifest: { name: "", version: "" } },
5050
definition: createTypeSpecLibrary({
51-
name: "@typespec/test",
51+
name: "@typespec/test-linter",
5252
diagnostics: {},
5353
}),
54-
linter: linterDef,
54+
linter: resolveLinterDefinition("@typespec/test-linter", linterDef),
5555
};
5656

5757
await host.compile("main.tsp");
@@ -212,6 +212,44 @@ describe("compiler: linter", () => {
212212
});
213213
});
214214

215+
describe("when enabling a ruleset", () => {
216+
it("/all ruleset is automatically provided and include all rules", async () => {
217+
const linter = await createTestLinter(`model Foo {}`, {
218+
rules: [noModelFoo],
219+
});
220+
expectDiagnosticEmpty(
221+
await linter.extendRuleSet({
222+
extends: ["@typespec/test-linter/all"],
223+
})
224+
);
225+
expectDiagnostics(linter.lint(), {
226+
severity: "warning",
227+
code: "@typespec/test-linter/no-model-foo",
228+
message: `Cannot call model 'Foo'`,
229+
});
230+
});
231+
it("extending specific ruleset enable the rules inside", async () => {
232+
const linter = await createTestLinter(`model Foo {}`, {
233+
rules: [noModelFoo],
234+
ruleSets: {
235+
custom: {
236+
enable: { "@typespec/test-linter/no-model-foo": true },
237+
},
238+
},
239+
});
240+
expectDiagnosticEmpty(
241+
await linter.extendRuleSet({
242+
extends: ["@typespec/test-linter/custom"],
243+
})
244+
);
245+
expectDiagnostics(linter.lint(), {
246+
severity: "warning",
247+
code: "@typespec/test-linter/no-model-foo",
248+
message: `Cannot call model 'Foo'`,
249+
});
250+
});
251+
});
252+
215253
describe("(integration) loading in program", () => {
216254
async function diagnoseReal(code: string) {
217255
const host = await createTestHost();

packages/http/src/linter.ts

-7
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,4 @@ import { opReferenceContainerRouteRule } from "./rules/op-reference-container-ro
33

44
export const $linter = defineLinter({
55
rules: [opReferenceContainerRouteRule],
6-
ruleSets: {
7-
all: {
8-
enable: {
9-
[`@typespec/http/${opReferenceContainerRouteRule.name}`]: true,
10-
},
11-
},
12-
},
136
});

packages/tspd/src/ref-doc/extractor.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
isTemplateDeclaration,
1717
joinPaths,
1818
JSONSchemaType,
19-
LinterDefinition,
19+
LinterResolvedDefinition,
2020
LinterRuleDefinition,
2121
LinterRuleSet,
2222
Model,
@@ -29,6 +29,7 @@ import {
2929
NoTarget,
3030
Operation,
3131
Program,
32+
resolveLinterDefinition,
3233
resolvePath,
3334
Scalar,
3435
SyntaxKind,
@@ -109,7 +110,7 @@ export async function extractLibraryRefDocs(
109110
// eslint-disable-next-line deprecation/deprecation
110111
const linter = entrypoint.$linter ?? lib?.linter;
111112
if (lib && linter) {
112-
refDoc.linter = extractLinterRefDoc(lib.name, linter);
113+
refDoc.linter = extractLinterRefDoc(lib.name, resolveLinterDefinition(lib.name, linter));
113114
}
114115
}
115116

@@ -622,7 +623,7 @@ function extractEmitterOptionsRefDoc(
622623
});
623624
}
624625

625-
function extractLinterRefDoc(libName: string, linter: LinterDefinition): LinterRefDoc {
626+
function extractLinterRefDoc(libName: string, linter: LinterResolvedDefinition): LinterRefDoc {
626627
return {
627628
ruleSets: linter.ruleSets && extractLinterRuleSetsRefDoc(libName, linter.ruleSets),
628629
rules: linter.rules.map((rule) => extractLinterRuleRefDoc(libName, rule)),

packages/tspd/src/ref-doc/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export type EmitterRefDoc = {
5151

5252
export type LinterRefDoc = {
5353
/** List of rulesets provided. */
54-
readonly ruleSets?: LinterRuleSetRefDoc[];
54+
readonly ruleSets: LinterRuleSetRefDoc[];
5555
readonly rules: LinterRuleRefDoc[];
5656
};
5757

0 commit comments

Comments
 (0)