Skip to content

Commit 1e410d4

Browse files
authored
[TypeSpec Validation] Must emit "@azure-tools/typespec-autorest" by default (#26314)
- Specs containing file "main.tsp" must emit "@azure-tools/typespec-autorest" by default - Also check compiler output for defense-in-depth - Fixes #25041
1 parent 8157606 commit 1e410d4

File tree

8 files changed

+167
-0
lines changed

8 files changed

+167
-0
lines changed

eng/tools/TypeSpecValidation/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { parseArgs, ParseArgsConfig } from "node:util";
22
import { CompileRule } from "./rules/compile.js";
3+
import { EmitAutorestRule } from "./rules/emit-autorest.js";
34
import { FolderStructureRule } from "./rules/folder-structure.js";
45
import { FormatRule } from "./rules/format.js";
56
import { GitDiffRule } from "./rules/git-diff.js";
@@ -25,6 +26,7 @@ export async function main() {
2526
const rules = [
2627
new FolderStructureRule(),
2728
new NpmPrefixRule(),
29+
new EmitAutorestRule(),
2830
new LinterRulesetRule(),
2931
new CompileRule(),
3032
new FormatRule(),

eng/tools/TypeSpecValidation/src/rules/compile.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ export class CompileRule implements Rule {
1717
`npx --no tsp compile . --warn-as-error`,
1818
folder,
1919
);
20+
if (
21+
stdout.toLowerCase().includes("no emitter was configured") ||
22+
stdout.toLowerCase().includes("no output was generated")
23+
) {
24+
success = false;
25+
errorOutput += "No emitter was configured and/or no output was generated.";
26+
}
2027
if (err) {
2128
success = false;
2229
errorOutput += err.message;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { join } from "path";
2+
import { parse as yamlParse } from "yaml";
3+
import { Rule } from "../rule.js";
4+
import { RuleResult } from "../rule-result.js";
5+
import { TsvHost } from "../tsv-host.js";
6+
7+
export class EmitAutorestRule implements Rule {
8+
readonly name = "EmitAutorest";
9+
10+
readonly description = 'Must emit "@azure-tools/typespec-autorest" by default';
11+
12+
async execute(host: TsvHost, folder: string): Promise<RuleResult> {
13+
let success = true;
14+
let stdOutput = "";
15+
let errorOutput = "";
16+
17+
const mainTspExists = await host.checkFileExists(join(folder, "main.tsp"));
18+
stdOutput += `mainTspExists: ${mainTspExists}\n`;
19+
20+
if (mainTspExists) {
21+
const configText = await host.readTspConfig(folder);
22+
const config = yamlParse(configText);
23+
24+
const emit = config?.emit;
25+
stdOutput += `emit: ${JSON.stringify(emit)}\n`;
26+
27+
if (!emit?.includes("@azure-tools/typespec-autorest")) {
28+
success = false;
29+
errorOutput +=
30+
"tspconfig.yaml must include the following emitter by default:\n" +
31+
"\n" +
32+
"emit:\n" +
33+
' - "@azure-tools/typespec-autorest"\n';
34+
}
35+
}
36+
37+
return {
38+
success: success,
39+
stdOutput: stdOutput,
40+
errorOutput: errorOutput,
41+
};
42+
}
43+
}

eng/tools/TypeSpecValidation/src/tsv-host.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export interface TsvHost {
22
checkFileExists(file: string): Promise<boolean>;
33
gitOperation(folder: string): IGitOperation;
4+
readTspConfig(folder: string): Promise<string>;
45
runCmd(cmd: string, cwd: string): Promise<[Error | null, string, string]>;
56
}
67

eng/tools/TypeSpecValidation/src/tsv-runner-host.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { join } from "path";
2+
import { readFile } from "fs/promises";
13
import { IGitOperation, TsvHost } from "./tsv-host.js";
24
import { simpleGit } from "simple-git";
35
import { runCmd, checkFileExists } from "./utils.js";
@@ -11,6 +13,10 @@ export class TsvRunnerHost implements TsvHost {
1113
return simpleGit(folder);
1214
}
1315

16+
readTspConfig(folder: string): Promise<string> {
17+
return readFile(join(folder, "tspconfig.yaml"), "utf-8");
18+
}
19+
1420
runCmd(cmd: string, cwd: string): Promise<[Error | null, string, string]> {
1521
return runCmd(cmd, cwd);
1622
}

eng/tools/TypeSpecValidation/test/compile.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,34 @@ describe("compile", function () {
77

88
assert(result.success);
99
});
10+
11+
it("should fail if no emitter was configured", async function () {
12+
let host = new TsvTestHost();
13+
host.runCmd = async (cmd: string, _cwd: string): Promise<[Error | null, string, string]> => {
14+
if (cmd.includes("tsp compile .")) {
15+
return [null, "no emitter was configured", ""];
16+
} else {
17+
return [null, "", ""];
18+
}
19+
};
20+
21+
const result = await new CompileRule().execute(host, TsvTestHost.folder);
22+
23+
assert(!result.success);
24+
});
25+
26+
it("should fail if no output was generated", async function () {
27+
let host = new TsvTestHost();
28+
host.runCmd = async (cmd: string, _cwd: string): Promise<[Error | null, string, string]> => {
29+
if (cmd.includes("tsp compile .")) {
30+
return [null, "no output was generated", ""];
31+
} else {
32+
return [null, "", ""];
33+
}
34+
};
35+
36+
const result = await new CompileRule().execute(host, TsvTestHost.folder);
37+
38+
assert(!result.success);
39+
});
1040
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { join } from "path";
2+
import { EmitAutorestRule } from "../src/rules/emit-autorest.js";
3+
import { TsvTestHost } from "./tsv-test-host.js";
4+
import { strict as assert } from "node:assert";
5+
6+
describe("emit-autorest", function () {
7+
it("should succeed if no main.tsp", async function () {
8+
let host = new TsvTestHost();
9+
host.checkFileExists = async (file: string) => file != join(TsvTestHost.folder, "main.tsp");
10+
11+
const result = await new EmitAutorestRule().execute(host, TsvTestHost.folder);
12+
13+
assert(result.success);
14+
});
15+
16+
it("should succeed if emits autorest", async function () {
17+
let host = new TsvTestHost();
18+
host.readTspConfig = async (_folder: string) => `
19+
emit:
20+
- "@azure-tools/typespec-autorest"
21+
`;
22+
23+
const result = await new EmitAutorestRule().execute(host, TsvTestHost.folder);
24+
25+
assert(result.success);
26+
});
27+
28+
it("should fail if config is empty", async function () {
29+
let host = new TsvTestHost();
30+
host.readTspConfig = async (_folder: string) => "";
31+
32+
const result = await new EmitAutorestRule().execute(host, TsvTestHost.folder);
33+
34+
assert(!result.success);
35+
});
36+
37+
it("should fail if no emit", async function () {
38+
let host = new TsvTestHost();
39+
host.readTspConfig = async (_folder: string) => `
40+
linter:
41+
extends:
42+
- "@azure-tools/typespec-azure-core/all"
43+
`;
44+
45+
const result = await new EmitAutorestRule().execute(host, TsvTestHost.folder);
46+
47+
assert(!result.success);
48+
});
49+
50+
it("should fail if no emit autorest", async function () {
51+
let host = new TsvTestHost();
52+
host.readTspConfig = async (_folder: string) => `
53+
emit:
54+
- foo
55+
`;
56+
57+
const result = await new EmitAutorestRule().execute(host, TsvTestHost.folder);
58+
59+
assert(!result.success);
60+
});
61+
});

eng/tools/TypeSpecValidation/test/tsv-test-host.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,21 @@ export class TsvTestHost implements TsvHost {
3333
async checkFileExists(_file: string): Promise<boolean> {
3434
return true;
3535
}
36+
37+
async readTspConfig(_folder: string): Promise<string> {
38+
// Sample config that should cause all rules to succeed
39+
return `
40+
emit:
41+
- "@azure-tools/typespec-autorest"
42+
linter:
43+
extends:
44+
- "@azure-tools/typespec-azure-core/all"
45+
options:
46+
"@azure-tools/typespec-autorest":
47+
azure-resource-provider-folder: "data-plane"
48+
emitter-output-dir: "{project-root}/.."
49+
examples-directory: "examples"
50+
output-file: "{azure-resource-provider-folder}/{service-name}/{version-status}/{version}/openapi.json"
51+
`;
52+
}
3653
}

0 commit comments

Comments
 (0)