From 6bb00d39a0409c4e9188223718536c7fca769194 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Dec 2024 08:30:16 +0000 Subject: [PATCH 01/93] WIP: Implement TSV as ESLint Rules --- eng/tools/eslint-plugin-tsv/package.json | 24 ++++++++++++++ .../eslint-plugin-tsv/src/enforce-foo-bar.cjs | 32 +++++++++++++++++++ .../src/eslint-plugin-tsv.cjs | 5 +++ .../eslint-plugin-tsv/test/eslint.config.cjs | 15 +++++++++ eng/tools/eslint-plugin-tsv/test/test.yaml | 1 + eng/tools/eslint-plugin-tsv/tsconfig.json | 6 ++++ 6 files changed, 83 insertions(+) create mode 100644 eng/tools/eslint-plugin-tsv/package.json create mode 100644 eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.cjs create mode 100644 eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.cjs create mode 100644 eng/tools/eslint-plugin-tsv/test/eslint.config.cjs create mode 100644 eng/tools/eslint-plugin-tsv/test/test.yaml create mode 100644 eng/tools/eslint-plugin-tsv/tsconfig.json diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json new file mode 100644 index 000000000000..b27f237b803f --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -0,0 +1,24 @@ +{ + "name": "@azure-tools/eslint-plugin-tsv", + "private": true, + "type": "module", + "main": "src/index.js", + "dependencies": { + "yaml-eslint-parser": "^1.2.3" + }, + "peerDependencies": { + "eslint": ">=9.0.0" + }, + "devDependencies": { + "@types/node": "^18.19.31", + "eslint": "^9.17.0", + "typescript": "~5.6.2" + }, + "scripts": { + "build": "tsc --build", + "test": "eslint --config test/eslint.config.cjs test/test.yaml" + }, + "engines": { + "node": ">= 18.0.0" + } +} diff --git a/eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.cjs b/eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.cjs new file mode 100644 index 000000000000..92ffd91d632c --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.cjs @@ -0,0 +1,32 @@ +// TODO: Convert to TS + +module.exports = { + meta: { + type: "problem", + docs: { + description: + "Enforce that a variable named `foo` can only be assigned a value of 'bar'.", + }, + fixable: "code", + schema: [], + }, + create(context) { + return { + YAMLPair(node) { + if (node.key.value == "foo" && node.value.value != "bar") { + context.report({ + node, + message: + 'Value other than "bar" assigned to `foo`. Unexpected value: {{ notBar }}.', + data: { + notBar: node.value.value, + }, + fix(fixer) { + return fixer.replaceText(node.value, 'bar'); + }, + }); + } + }, + }; + }, +}; diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.cjs b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.cjs new file mode 100644 index 000000000000..7652b9759d93 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.cjs @@ -0,0 +1,5 @@ +// TODO: Convert to TS + +const enforceFooBar = require("./enforce-foo-bar.cjs"); +const plugin = { rules: { "enforce-foo-bar": enforceFooBar } }; +module.exports = plugin; diff --git a/eng/tools/eslint-plugin-tsv/test/eslint.config.cjs b/eng/tools/eslint-plugin-tsv/test/eslint.config.cjs new file mode 100644 index 000000000000..19a942ee144c --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/test/eslint.config.cjs @@ -0,0 +1,15 @@ +const eslintPluginTsv = require("../src/eslint-plugin-tsv.cjs"); +const parser = require("yaml-eslint-parser"); + +module.exports = [ + { + plugins: { tsv: eslintPluginTsv }, + files: ["*.yaml", "**/*.yaml"], + languageOptions: { + parser, + }, + rules: { + "tsv/enforce-foo-bar": "error", + }, + }, +]; diff --git a/eng/tools/eslint-plugin-tsv/test/test.yaml b/eng/tools/eslint-plugin-tsv/test/test.yaml new file mode 100644 index 000000000000..c444f32c5010 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/test/test.yaml @@ -0,0 +1 @@ +foo: baz diff --git a/eng/tools/eslint-plugin-tsv/tsconfig.json b/eng/tools/eslint-plugin-tsv/tsconfig.json new file mode 100644 index 000000000000..ec6d6640928a --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + } +} From 2b9965f136145fe568bfb12cc2588bf5ec00e752 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Dec 2024 08:56:38 +0000 Subject: [PATCH 02/93] Convert to TS (but still cjs) --- eng/tools/eslint-plugin-tsv/package.json | 2 +- .../src/{enforce-foo-bar.cjs => enforce-foo-bar.ts} | 5 +++-- .../src/{eslint-plugin-tsv.cjs => eslint-plugin-tsv.ts} | 4 ++-- eng/tools/eslint-plugin-tsv/test/eslint.config.cjs | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) rename eng/tools/eslint-plugin-tsv/src/{enforce-foo-bar.cjs => enforce-foo-bar.ts} (92%) rename eng/tools/eslint-plugin-tsv/src/{eslint-plugin-tsv.cjs => eslint-plugin-tsv.ts} (53%) diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index b27f237b803f..cde9b843ac8b 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -1,7 +1,7 @@ { "name": "@azure-tools/eslint-plugin-tsv", "private": true, - "type": "module", + "TODO-type": "module", "main": "src/index.js", "dependencies": { "yaml-eslint-parser": "^1.2.3" diff --git a/eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.cjs b/eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.ts similarity index 92% rename from eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.cjs rename to eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.ts index 92ffd91d632c..efaf05a01d21 100644 --- a/eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.cjs +++ b/eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.ts @@ -1,5 +1,3 @@ -// TODO: Convert to TS - module.exports = { meta: { type: "problem", @@ -10,8 +8,10 @@ module.exports = { fixable: "code", schema: [], }, + // @ts-ignore create(context) { return { + // @ts-ignore YAMLPair(node) { if (node.key.value == "foo" && node.value.value != "bar") { context.report({ @@ -21,6 +21,7 @@ module.exports = { data: { notBar: node.value.value, }, + // @ts-ignore fix(fixer) { return fixer.replaceText(node.value, 'bar'); }, diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.cjs b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts similarity index 53% rename from eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.cjs rename to eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts index 7652b9759d93..d05d6b0f2684 100644 --- a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.cjs +++ b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -1,5 +1,5 @@ -// TODO: Convert to TS +// @ts-ignore +import enforceFooBar from "./enforce-foo-bar"; -const enforceFooBar = require("./enforce-foo-bar.cjs"); const plugin = { rules: { "enforce-foo-bar": enforceFooBar } }; module.exports = plugin; diff --git a/eng/tools/eslint-plugin-tsv/test/eslint.config.cjs b/eng/tools/eslint-plugin-tsv/test/eslint.config.cjs index 19a942ee144c..bf779d80b834 100644 --- a/eng/tools/eslint-plugin-tsv/test/eslint.config.cjs +++ b/eng/tools/eslint-plugin-tsv/test/eslint.config.cjs @@ -1,4 +1,4 @@ -const eslintPluginTsv = require("../src/eslint-plugin-tsv.cjs"); +const eslintPluginTsv = require("../dist/src/eslint-plugin-tsv.js"); const parser = require("yaml-eslint-parser"); module.exports = [ From 1ae2452a552adc8ea25e9561ebc6938e4b3d33eb Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Dec 2024 09:12:10 +0000 Subject: [PATCH 03/93] Convert to mjs --- eng/tools/eslint-plugin-tsv/package.json | 4 +- .../eslint-plugin-tsv/src/enforce-foo-bar.ts | 4 +- .../src/eslint-plugin-tsv.ts | 6 +- .../{eslint.config.cjs => eslint.config.ts} | 8 ++- eng/tools/package.json | 1 + eng/tools/tsconfig.json | 1 + package-lock.json | 70 ++++++++++++++++--- 7 files changed, 77 insertions(+), 17 deletions(-) rename eng/tools/eslint-plugin-tsv/test/{eslint.config.cjs => eslint.config.ts} (56%) diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index cde9b843ac8b..9b71d5104535 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -1,7 +1,7 @@ { "name": "@azure-tools/eslint-plugin-tsv", "private": true, - "TODO-type": "module", + "type": "module", "main": "src/index.js", "dependencies": { "yaml-eslint-parser": "^1.2.3" @@ -16,7 +16,7 @@ }, "scripts": { "build": "tsc --build", - "test": "eslint --config test/eslint.config.cjs test/test.yaml" + "test": "eslint --config dist/test/eslint.config.js test/test.yaml" }, "engines": { "node": ">= 18.0.0" diff --git a/eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.ts b/eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.ts index efaf05a01d21..81145fe52414 100644 --- a/eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.ts +++ b/eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.ts @@ -1,4 +1,4 @@ -module.exports = { +export const rule = { meta: { type: "problem", docs: { @@ -31,3 +31,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts index d05d6b0f2684..9edfef2e76b7 100644 --- a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts +++ b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -1,5 +1,5 @@ // @ts-ignore -import enforceFooBar from "./enforce-foo-bar"; +import enforceFooBar from "./enforce-foo-bar.js"; -const plugin = { rules: { "enforce-foo-bar": enforceFooBar } }; -module.exports = plugin; +export const plugin = { rules: { "enforce-foo-bar": enforceFooBar } }; +export default plugin; diff --git a/eng/tools/eslint-plugin-tsv/test/eslint.config.cjs b/eng/tools/eslint-plugin-tsv/test/eslint.config.ts similarity index 56% rename from eng/tools/eslint-plugin-tsv/test/eslint.config.cjs rename to eng/tools/eslint-plugin-tsv/test/eslint.config.ts index bf779d80b834..88e5b14a5596 100644 --- a/eng/tools/eslint-plugin-tsv/test/eslint.config.cjs +++ b/eng/tools/eslint-plugin-tsv/test/eslint.config.ts @@ -1,7 +1,7 @@ -const eslintPluginTsv = require("../dist/src/eslint-plugin-tsv.js"); -const parser = require("yaml-eslint-parser"); +import eslintPluginTsv from "../src/eslint-plugin-tsv.js"; +import parser from "yaml-eslint-parser"; -module.exports = [ +export const config = [ { plugins: { tsv: eslintPluginTsv }, files: ["*.yaml", "**/*.yaml"], @@ -13,3 +13,5 @@ module.exports = [ }, }, ]; + +export default config; diff --git a/eng/tools/package.json b/eng/tools/package.json index 8dcd0c6a9d2d..298f6cf8c821 100644 --- a/eng/tools/package.json +++ b/eng/tools/package.json @@ -1,6 +1,7 @@ { "name": "azure-rest-api-specs-eng-tools", "devDependencies": { + "@azure-tools/eslint-plugin-tsv": "file:eslint-plugin-tsv", "@azure-tools/specs-model": "file:specs-model", "@azure-tools/suppressions": "file:suppressions", "@azure-tools/tsp-client-tests": "file:tsp-client-tests", diff --git a/eng/tools/tsconfig.json b/eng/tools/tsconfig.json index ffa89e56a6d8..008ddb8c064d 100644 --- a/eng/tools/tsconfig.json +++ b/eng/tools/tsconfig.json @@ -11,6 +11,7 @@ "composite": true, }, "references": [ + { "path": "./eslint-plugin-tsv" }, { "path": "./specs-model" }, { "path": "./suppressions" }, { "path": "./tsp-client-tests" }, diff --git a/package-lock.json b/package-lock.json index 4ff3c2f2b07d..f3328e3ac5ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "dev": true, "hasInstallScript": true, "devDependencies": { + "@azure-tools/eslint-plugin-tsv": "file:eslint-plugin-tsv", "@azure-tools/specs-model": "file:specs-model", "@azure-tools/suppressions": "file:suppressions", "@azure-tools/tsp-client-tests": "file:tsp-client-tests", @@ -46,6 +47,24 @@ "@azure-tools/typespec-validation": "file:typespec-validation" } }, + "eng/tools/eslint-plugin-tsv": { + "name": "@azure-tools/eslint-plugin-tsv", + "dev": true, + "dependencies": { + "yaml-eslint-parser": "^1.2.3" + }, + "devDependencies": { + "@types/node": "^18.19.31", + "eslint": "^9.17.0", + "typescript": "~5.6.2" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0" + } + }, "eng/tools/node_modules/@types/node": { "version": "18.19.68", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.68.tgz", @@ -818,6 +837,10 @@ "node": ">=12.0.0" } }, + "node_modules/@azure-tools/eslint-plugin-tsv": { + "resolved": "eng/tools/eslint-plugin-tsv", + "link": true + }, "node_modules/@azure-tools/openapi-tools-common": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@azure-tools/openapi-tools-common/-/openapi-tools-common-1.2.2.tgz", @@ -2234,9 +2257,9 @@ "license": "MIT" }, "node_modules/@eslint/js": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", - "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", + "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", "dev": true, "license": "MIT", "engines": { @@ -4956,9 +4979,9 @@ } }, "node_modules/eslint": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", - "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", + "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", "dev": true, "license": "MIT", "dependencies": { @@ -4967,7 +4990,7 @@ "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.9.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.16.0", + "@eslint/js": "9.17.0", "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -4976,7 +4999,7 @@ "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.5", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", @@ -10023,6 +10046,37 @@ "node": ">= 14" } }, + "node_modules/yaml-eslint-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/yaml-eslint-parser/-/yaml-eslint-parser-1.2.3.tgz", + "integrity": "sha512-4wZWvE398hCP7O8n3nXKu/vdq1HcH01ixYlCREaJL5NUMwQ0g3MaGFUBNSlmBtKmhbtVG/Cm6lyYmSVTEVil8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.0.0", + "lodash": "^4.17.21", + "yaml": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, + "node_modules/yaml-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", From 57655ba61a5d8140fc4cf0ffc99d4faa5ad51de7 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Dec 2024 09:30:34 +0000 Subject: [PATCH 04/93] Add contoso test --- eng/tools/eslint-plugin-tsv/package.json | 3 +- ...int.config.ts => contoso.eslint.config.ts} | 0 .../test/contoso.tspconfig.yaml | 39 +++++++++++++++++++ .../test/test.eslint.config.ts | 17 ++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) rename eng/tools/eslint-plugin-tsv/test/{eslint.config.ts => contoso.eslint.config.ts} (100%) create mode 100644 eng/tools/eslint-plugin-tsv/test/contoso.tspconfig.yaml create mode 100644 eng/tools/eslint-plugin-tsv/test/test.eslint.config.ts diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index 9b71d5104535..edd3a608e216 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -16,7 +16,8 @@ }, "scripts": { "build": "tsc --build", - "test": "eslint --config dist/test/eslint.config.js test/test.yaml" + "test": "eslint --config dist/test/test.eslint.config.js test/test.yaml", + "test:contoso": "eslint --config dist/test/contoso.eslint.config.js test/contoso.tspconfig.yaml" }, "engines": { "node": ">= 18.0.0" diff --git a/eng/tools/eslint-plugin-tsv/test/eslint.config.ts b/eng/tools/eslint-plugin-tsv/test/contoso.eslint.config.ts similarity index 100% rename from eng/tools/eslint-plugin-tsv/test/eslint.config.ts rename to eng/tools/eslint-plugin-tsv/test/contoso.eslint.config.ts diff --git a/eng/tools/eslint-plugin-tsv/test/contoso.tspconfig.yaml b/eng/tools/eslint-plugin-tsv/test/contoso.tspconfig.yaml new file mode 100644 index 000000000000..2633b3a76f34 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/test/contoso.tspconfig.yaml @@ -0,0 +1,39 @@ +parameters: + "service-dir": + default: "sdk/contosowidgetmanager" + "dependencies": + "additionalDirectories": + - "specification/contosowidgetmanager/Contoso.WidgetManager.Shared/" + default: "" +emit: + - "@azure-tools/typespec-autorest" +linter: + extends: + - "@azure-tools/typespec-azure-rulesets/data-plane" +options: + "@azure-tools/typespec-autorest": + azure-resource-provider-folder: "data-plane" + emit-lro-options: "none" + emitter-output-dir: "{project-root}/.." + output-file: "{azure-resource-provider-folder}/{service-name}/{version-status}/{version}/widgets.json" + "@azure-tools/typespec-python": + package-dir: "azure-contoso-widgetmanager" + package-name: "{package-dir}" + generate-test: true + generate-sample: true + flavor: azure + "@azure-tools/typespec-csharp": + package-dir: "Azure.Template.Contoso" + clear-output-folder: true + model-namespace: false + namespace: "{package-dir}" + flavor: azure + "@azure-tools/typespec-ts": + package-dir: "contosowidgetmanager-rest" + packageDetails: + name: "@azure-rest/contoso-widgetmanager-rest" + flavor: azure + "@azure-tools/typespec-java": + package-dir: "azure-contoso-widgetmanager" + namespace: com.azure.contoso.widgetmanager + flavor: azure diff --git a/eng/tools/eslint-plugin-tsv/test/test.eslint.config.ts b/eng/tools/eslint-plugin-tsv/test/test.eslint.config.ts new file mode 100644 index 000000000000..88e5b14a5596 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/test/test.eslint.config.ts @@ -0,0 +1,17 @@ +import eslintPluginTsv from "../src/eslint-plugin-tsv.js"; +import parser from "yaml-eslint-parser"; + +export const config = [ + { + plugins: { tsv: eslintPluginTsv }, + files: ["*.yaml", "**/*.yaml"], + languageOptions: { + parser, + }, + rules: { + "tsv/enforce-foo-bar": "error", + }, + }, +]; + +export default config; From 06153631beb4e5c9bfed0280725d1017f2a41728 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Dec 2024 09:53:51 +0000 Subject: [PATCH 05/93] Add rule top-level-folder-lowercase --- eng/tools/eslint-plugin-tsv/package.json | 4 +- .../src/eslint-plugin-tsv.ts | 8 +++- .../src/top-level-folder-lowercase.ts | 29 ++++++++++++++ .../test/contoso.eslint.config.ts | 17 -------- .../Contoso.WidgetManager/tspconfig.yaml} | 0 .../Contoso.WidgetManager/tspconfig.yaml | 39 +++++++++++++++++++ .../test/specification/eslint.config.ts | 18 +++++++++ 7 files changed, 96 insertions(+), 19 deletions(-) create mode 100644 eng/tools/eslint-plugin-tsv/src/top-level-folder-lowercase.ts rename eng/tools/eslint-plugin-tsv/test/{contoso.tspconfig.yaml => specification/InvalidCase/Contoso.WidgetManager/tspconfig.yaml} (100%) create mode 100644 eng/tools/eslint-plugin-tsv/test/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml create mode 100644 eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index edd3a608e216..ae0cd1e6240d 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -17,7 +17,9 @@ "scripts": { "build": "tsc --build", "test": "eslint --config dist/test/test.eslint.config.js test/test.yaml", - "test:contoso": "eslint --config dist/test/contoso.eslint.config.js test/contoso.tspconfig.yaml" + "test:contoso": "eslint --config dist/test/specification/eslint.config.js test/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", + "test:top-level-folder-mixed-case": "eslint --config dist/test/specification/eslint.config.js test/specification/InvalidCase/Contoso.WidgetManager/tspconfig.yaml", + "test:all": "npm run test; npm run test:contoso; npm run test:top-level-folder-mixed-case" }, "engines": { "node": ">= 18.0.0" diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts index 9edfef2e76b7..ce4e4443a5c0 100644 --- a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts +++ b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -1,5 +1,11 @@ // @ts-ignore import enforceFooBar from "./enforce-foo-bar.js"; +import topLevelFolderLowercase from "./top-level-folder-lowercase.js"; -export const plugin = { rules: { "enforce-foo-bar": enforceFooBar } }; +export const plugin = { + rules: { + "enforce-foo-bar": enforceFooBar, + "top-level-folder-lowercase": topLevelFolderLowercase, + }, +}; export default plugin; diff --git a/eng/tools/eslint-plugin-tsv/src/top-level-folder-lowercase.ts b/eng/tools/eslint-plugin-tsv/src/top-level-folder-lowercase.ts new file mode 100644 index 000000000000..887d91e029e2 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/top-level-folder-lowercase.ts @@ -0,0 +1,29 @@ +export const rule = { + meta: { + type: "problem", + docs: { + description: "Enforce that top level folder under 'specification' is lower case.", + }, + schema: [], + }, + // @ts-ignore + create(context) { + const filename = context.getFilename() as string; + const regex = /specification\/[^/]*[A-Z]+[^/]*\//; + + return { + // @ts-ignore + Program(node) { + // Check if the filename ends with '.test.js' + if (filename.match(regex)) { + context.report({ + node, + message: "invalidPath", + }); + } + }, + }; + }, +}; + +export default rule; diff --git a/eng/tools/eslint-plugin-tsv/test/contoso.eslint.config.ts b/eng/tools/eslint-plugin-tsv/test/contoso.eslint.config.ts index 88e5b14a5596..e69de29bb2d1 100644 --- a/eng/tools/eslint-plugin-tsv/test/contoso.eslint.config.ts +++ b/eng/tools/eslint-plugin-tsv/test/contoso.eslint.config.ts @@ -1,17 +0,0 @@ -import eslintPluginTsv from "../src/eslint-plugin-tsv.js"; -import parser from "yaml-eslint-parser"; - -export const config = [ - { - plugins: { tsv: eslintPluginTsv }, - files: ["*.yaml", "**/*.yaml"], - languageOptions: { - parser, - }, - rules: { - "tsv/enforce-foo-bar": "error", - }, - }, -]; - -export default config; diff --git a/eng/tools/eslint-plugin-tsv/test/contoso.tspconfig.yaml b/eng/tools/eslint-plugin-tsv/test/specification/InvalidCase/Contoso.WidgetManager/tspconfig.yaml similarity index 100% rename from eng/tools/eslint-plugin-tsv/test/contoso.tspconfig.yaml rename to eng/tools/eslint-plugin-tsv/test/specification/InvalidCase/Contoso.WidgetManager/tspconfig.yaml diff --git a/eng/tools/eslint-plugin-tsv/test/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml b/eng/tools/eslint-plugin-tsv/test/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml new file mode 100644 index 000000000000..2633b3a76f34 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/test/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml @@ -0,0 +1,39 @@ +parameters: + "service-dir": + default: "sdk/contosowidgetmanager" + "dependencies": + "additionalDirectories": + - "specification/contosowidgetmanager/Contoso.WidgetManager.Shared/" + default: "" +emit: + - "@azure-tools/typespec-autorest" +linter: + extends: + - "@azure-tools/typespec-azure-rulesets/data-plane" +options: + "@azure-tools/typespec-autorest": + azure-resource-provider-folder: "data-plane" + emit-lro-options: "none" + emitter-output-dir: "{project-root}/.." + output-file: "{azure-resource-provider-folder}/{service-name}/{version-status}/{version}/widgets.json" + "@azure-tools/typespec-python": + package-dir: "azure-contoso-widgetmanager" + package-name: "{package-dir}" + generate-test: true + generate-sample: true + flavor: azure + "@azure-tools/typespec-csharp": + package-dir: "Azure.Template.Contoso" + clear-output-folder: true + model-namespace: false + namespace: "{package-dir}" + flavor: azure + "@azure-tools/typespec-ts": + package-dir: "contosowidgetmanager-rest" + packageDetails: + name: "@azure-rest/contoso-widgetmanager-rest" + flavor: azure + "@azure-tools/typespec-java": + package-dir: "azure-contoso-widgetmanager" + namespace: com.azure.contoso.widgetmanager + flavor: azure diff --git a/eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts b/eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts new file mode 100644 index 000000000000..c885798b20d1 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts @@ -0,0 +1,18 @@ +import eslintPluginTsv from "../../src/eslint-plugin-tsv.js"; +import parser from "yaml-eslint-parser"; + +export const config = [ + { + plugins: { tsv: eslintPluginTsv }, + files: ["*.yaml", "**/*.yaml"], + languageOptions: { + parser, + }, + rules: { + "tsv/enforce-foo-bar": "error", + "tsv/top-level-folder-lowercase": "error", + }, + }, +]; + +export default config; From 6e44905cc7a5defe8bbad676e21282f47d443047 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Dec 2024 09:56:01 +0000 Subject: [PATCH 06/93] build before test --- eng/tools/eslint-plugin-tsv/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index ae0cd1e6240d..93ce29a093e1 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -19,7 +19,7 @@ "test": "eslint --config dist/test/test.eslint.config.js test/test.yaml", "test:contoso": "eslint --config dist/test/specification/eslint.config.js test/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", "test:top-level-folder-mixed-case": "eslint --config dist/test/specification/eslint.config.js test/specification/InvalidCase/Contoso.WidgetManager/tspconfig.yaml", - "test:all": "npm run test; npm run test:contoso; npm run test:top-level-folder-mixed-case" + "test:all": "npm run build; npm run test; npm run test:contoso; npm run test:top-level-folder-mixed-case" }, "engines": { "node": ">= 18.0.0" From ae8a8a5a0b18a93dc798d94854b743f88402321b Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Dec 2024 10:55:03 +0000 Subject: [PATCH 07/93] Rename rule to align with eslint conventions --- .../src/eslint-plugin-tsv.ts | 4 +-- .../src/no-uppercase-under-specification.ts | 34 +++++++++++++++++++ .../src/top-level-folder-lowercase.ts | 29 ---------------- .../test/contoso.eslint.config.ts | 0 .../test/specification/eslint.config.ts | 2 +- 5 files changed, 37 insertions(+), 32 deletions(-) create mode 100644 eng/tools/eslint-plugin-tsv/src/no-uppercase-under-specification.ts delete mode 100644 eng/tools/eslint-plugin-tsv/src/top-level-folder-lowercase.ts delete mode 100644 eng/tools/eslint-plugin-tsv/test/contoso.eslint.config.ts diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts index ce4e4443a5c0..450a4b54628b 100644 --- a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts +++ b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -1,11 +1,11 @@ // @ts-ignore import enforceFooBar from "./enforce-foo-bar.js"; -import topLevelFolderLowercase from "./top-level-folder-lowercase.js"; +import noUppercaseUnderSpecification from "./no-uppercase-under-specification.js"; export const plugin = { rules: { "enforce-foo-bar": enforceFooBar, - "top-level-folder-lowercase": topLevelFolderLowercase, + "no-uppercase-under-specification": noUppercaseUnderSpecification, }, }; export default plugin; diff --git a/eng/tools/eslint-plugin-tsv/src/no-uppercase-under-specification.ts b/eng/tools/eslint-plugin-tsv/src/no-uppercase-under-specification.ts new file mode 100644 index 000000000000..52469942ecee --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/no-uppercase-under-specification.ts @@ -0,0 +1,34 @@ +export const rule = { + meta: { + type: "problem", + docs: { + description: "Disallow uppercase letters in first folder under '/specification/'", + }, + schema: [], + messages: { + upper: "'{{path}}' contains uppercase letters in the first folder under '/specification/'", + }, + }, + // @ts-ignore + create(context) { + const filename = context.getFilename() as string; + + const regex = /\/specification\/[^/A-Z]+\//; + const uppercaseLettersInFirstFolder = !filename.match(regex); + + return { + // @ts-ignore + Program(node) { + if (uppercaseLettersInFirstFolder) { + context.report({ + node, + messageId: "upper", + data: { path: filename }, + }); + } + }, + }; + }, +}; + +export default rule; diff --git a/eng/tools/eslint-plugin-tsv/src/top-level-folder-lowercase.ts b/eng/tools/eslint-plugin-tsv/src/top-level-folder-lowercase.ts deleted file mode 100644 index 887d91e029e2..000000000000 --- a/eng/tools/eslint-plugin-tsv/src/top-level-folder-lowercase.ts +++ /dev/null @@ -1,29 +0,0 @@ -export const rule = { - meta: { - type: "problem", - docs: { - description: "Enforce that top level folder under 'specification' is lower case.", - }, - schema: [], - }, - // @ts-ignore - create(context) { - const filename = context.getFilename() as string; - const regex = /specification\/[^/]*[A-Z]+[^/]*\//; - - return { - // @ts-ignore - Program(node) { - // Check if the filename ends with '.test.js' - if (filename.match(regex)) { - context.report({ - node, - message: "invalidPath", - }); - } - }, - }; - }, -}; - -export default rule; diff --git a/eng/tools/eslint-plugin-tsv/test/contoso.eslint.config.ts b/eng/tools/eslint-plugin-tsv/test/contoso.eslint.config.ts deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts b/eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts index c885798b20d1..a4ffcf819285 100644 --- a/eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts +++ b/eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts @@ -10,7 +10,7 @@ export const config = [ }, rules: { "tsv/enforce-foo-bar": "error", - "tsv/top-level-folder-lowercase": "error", + "tsv/no-uppercase-under-specification": "error", }, }, ]; From 79256c4d182ad73330f488eac3b4838b5aa15915 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Dec 2024 11:15:44 +0000 Subject: [PATCH 08/93] Rename rule --- .../src/eslint-plugin-tsv.ts | 4 +-- .../src/kebab-case-first-path-segment.ts | 33 ++++++++++++++++++ .../src/no-uppercase-under-specification.ts | 34 ------------------- .../test/specification/eslint.config.ts | 2 +- 4 files changed, 36 insertions(+), 37 deletions(-) create mode 100644 eng/tools/eslint-plugin-tsv/src/kebab-case-first-path-segment.ts delete mode 100644 eng/tools/eslint-plugin-tsv/src/no-uppercase-under-specification.ts diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts index 450a4b54628b..eeeed694819e 100644 --- a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts +++ b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -1,11 +1,11 @@ // @ts-ignore import enforceFooBar from "./enforce-foo-bar.js"; -import noUppercaseUnderSpecification from "./no-uppercase-under-specification.js"; +import kebabCaseFirstPathSegment from "./kebab-case-first-path-segment.js"; export const plugin = { rules: { "enforce-foo-bar": enforceFooBar, - "no-uppercase-under-specification": noUppercaseUnderSpecification, + "kebab-case-first-path-segment": kebabCaseFirstPathSegment, }, }; export default plugin; diff --git a/eng/tools/eslint-plugin-tsv/src/kebab-case-first-path-segment.ts b/eng/tools/eslint-plugin-tsv/src/kebab-case-first-path-segment.ts new file mode 100644 index 000000000000..7f5fb04b631e --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/kebab-case-first-path-segment.ts @@ -0,0 +1,33 @@ +export const rule = { + meta: { + type: "problem", + docs: { + description: "Requires first path segment after 'specification' to use kebab-case", + }, + schema: [], + messages: { + kebab: "First path segment after 'specification' does not use kebab-case", + }, + }, + // @ts-ignore + create(context) { + const filename = context.getFilename() as string; + + const regex = /\/specification\/[a-z0-9]+(-[a-z0-9]+)*\//; + const kebabCaseFirstFolder = filename.match(regex); + + return { + // @ts-ignore + Program(node) { + if (!kebabCaseFirstFolder) { + context.report({ + node, + messageId: "kebab", + }); + } + }, + }; + }, +}; + +export default rule; diff --git a/eng/tools/eslint-plugin-tsv/src/no-uppercase-under-specification.ts b/eng/tools/eslint-plugin-tsv/src/no-uppercase-under-specification.ts deleted file mode 100644 index 52469942ecee..000000000000 --- a/eng/tools/eslint-plugin-tsv/src/no-uppercase-under-specification.ts +++ /dev/null @@ -1,34 +0,0 @@ -export const rule = { - meta: { - type: "problem", - docs: { - description: "Disallow uppercase letters in first folder under '/specification/'", - }, - schema: [], - messages: { - upper: "'{{path}}' contains uppercase letters in the first folder under '/specification/'", - }, - }, - // @ts-ignore - create(context) { - const filename = context.getFilename() as string; - - const regex = /\/specification\/[^/A-Z]+\//; - const uppercaseLettersInFirstFolder = !filename.match(regex); - - return { - // @ts-ignore - Program(node) { - if (uppercaseLettersInFirstFolder) { - context.report({ - node, - messageId: "upper", - data: { path: filename }, - }); - } - }, - }; - }, -}; - -export default rule; diff --git a/eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts b/eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts index a4ffcf819285..544fb6ea7ffd 100644 --- a/eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts +++ b/eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts @@ -10,7 +10,7 @@ export const config = [ }, rules: { "tsv/enforce-foo-bar": "error", - "tsv/no-uppercase-under-specification": "error", + "tsv/kebab-case-first-path-segment": "error", }, }, ]; From 85db15861501d53ecef7d8cf92f2bdb4a5eebde0 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Dec 2024 11:19:31 +0000 Subject: [PATCH 09/93] Improve test --- eng/tools/eslint-plugin-tsv/package.json | 4 ++-- .../Not.KebabCase}/tspconfig.yaml | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename eng/tools/eslint-plugin-tsv/test/specification/{InvalidCase/Contoso.WidgetManager => Not-Kebab-Case/Not.KebabCase}/tspconfig.yaml (100%) diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index 93ce29a093e1..b675154c60bf 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -18,8 +18,8 @@ "build": "tsc --build", "test": "eslint --config dist/test/test.eslint.config.js test/test.yaml", "test:contoso": "eslint --config dist/test/specification/eslint.config.js test/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", - "test:top-level-folder-mixed-case": "eslint --config dist/test/specification/eslint.config.js test/specification/InvalidCase/Contoso.WidgetManager/tspconfig.yaml", - "test:all": "npm run build; npm run test; npm run test:contoso; npm run test:top-level-folder-mixed-case" + "test:not-kebab-case": "eslint --config dist/test/specification/eslint.config.js test/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", + "test:all": "npm run build; npm run test; npm run test:contoso; npm run test:not-kebab-case" }, "engines": { "node": ">= 18.0.0" diff --git a/eng/tools/eslint-plugin-tsv/test/specification/InvalidCase/Contoso.WidgetManager/tspconfig.yaml b/eng/tools/eslint-plugin-tsv/test/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml similarity index 100% rename from eng/tools/eslint-plugin-tsv/test/specification/InvalidCase/Contoso.WidgetManager/tspconfig.yaml rename to eng/tools/eslint-plugin-tsv/test/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml From 497387870cddee37aba0047004bb08c2f89d776d Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Dec 2024 21:30:24 +0000 Subject: [PATCH 10/93] Remove sample rule --- eng/tools/eslint-plugin-tsv/package.json | 3 +- .../eslint-plugin-tsv/src/enforce-foo-bar.ts | 35 ------------------- .../src/eslint-plugin-tsv.ts | 3 -- .../test/specification/eslint.config.ts | 1 - .../test/test.eslint.config.ts | 17 --------- eng/tools/eslint-plugin-tsv/test/test.yaml | 1 - 6 files changed, 1 insertion(+), 59 deletions(-) delete mode 100644 eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.ts delete mode 100644 eng/tools/eslint-plugin-tsv/test/test.eslint.config.ts delete mode 100644 eng/tools/eslint-plugin-tsv/test/test.yaml diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index b675154c60bf..61d6e6e955f9 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -16,10 +16,9 @@ }, "scripts": { "build": "tsc --build", - "test": "eslint --config dist/test/test.eslint.config.js test/test.yaml", "test:contoso": "eslint --config dist/test/specification/eslint.config.js test/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", "test:not-kebab-case": "eslint --config dist/test/specification/eslint.config.js test/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", - "test:all": "npm run build; npm run test; npm run test:contoso; npm run test:not-kebab-case" + "test:all": "npm run build; npm run test:contoso; npm run test:not-kebab-case" }, "engines": { "node": ">= 18.0.0" diff --git a/eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.ts b/eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.ts deleted file mode 100644 index 81145fe52414..000000000000 --- a/eng/tools/eslint-plugin-tsv/src/enforce-foo-bar.ts +++ /dev/null @@ -1,35 +0,0 @@ -export const rule = { - meta: { - type: "problem", - docs: { - description: - "Enforce that a variable named `foo` can only be assigned a value of 'bar'.", - }, - fixable: "code", - schema: [], - }, - // @ts-ignore - create(context) { - return { - // @ts-ignore - YAMLPair(node) { - if (node.key.value == "foo" && node.value.value != "bar") { - context.report({ - node, - message: - 'Value other than "bar" assigned to `foo`. Unexpected value: {{ notBar }}.', - data: { - notBar: node.value.value, - }, - // @ts-ignore - fix(fixer) { - return fixer.replaceText(node.value, 'bar'); - }, - }); - } - }, - }; - }, -}; - -export default rule; diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts index eeeed694819e..c60b21834ec8 100644 --- a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts +++ b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -1,10 +1,7 @@ -// @ts-ignore -import enforceFooBar from "./enforce-foo-bar.js"; import kebabCaseFirstPathSegment from "./kebab-case-first-path-segment.js"; export const plugin = { rules: { - "enforce-foo-bar": enforceFooBar, "kebab-case-first-path-segment": kebabCaseFirstPathSegment, }, }; diff --git a/eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts b/eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts index 544fb6ea7ffd..ca4f81f688f3 100644 --- a/eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts +++ b/eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts @@ -9,7 +9,6 @@ export const config = [ parser, }, rules: { - "tsv/enforce-foo-bar": "error", "tsv/kebab-case-first-path-segment": "error", }, }, diff --git a/eng/tools/eslint-plugin-tsv/test/test.eslint.config.ts b/eng/tools/eslint-plugin-tsv/test/test.eslint.config.ts deleted file mode 100644 index 88e5b14a5596..000000000000 --- a/eng/tools/eslint-plugin-tsv/test/test.eslint.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import eslintPluginTsv from "../src/eslint-plugin-tsv.js"; -import parser from "yaml-eslint-parser"; - -export const config = [ - { - plugins: { tsv: eslintPluginTsv }, - files: ["*.yaml", "**/*.yaml"], - languageOptions: { - parser, - }, - rules: { - "tsv/enforce-foo-bar": "error", - }, - }, -]; - -export default config; diff --git a/eng/tools/eslint-plugin-tsv/test/test.yaml b/eng/tools/eslint-plugin-tsv/test/test.yaml deleted file mode 100644 index c444f32c5010..000000000000 --- a/eng/tools/eslint-plugin-tsv/test/test.yaml +++ /dev/null @@ -1 +0,0 @@ -foo: baz From 89006ec56791f7439bb2c819db86a1f8711b2559 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Dec 2024 21:37:22 +0000 Subject: [PATCH 11/93] Move e2e tests to separate folder --- eng/tools/eslint-plugin-tsv/package.json | 6 +++--- .../Not-Kebab-Case/Not.KebabCase/tspconfig.yaml | 0 .../Contoso.WidgetManager/tspconfig.yaml | 0 .../test/{ => e2e}/specification/eslint.config.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename eng/tools/eslint-plugin-tsv/test/{ => e2e}/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml (100%) rename eng/tools/eslint-plugin-tsv/test/{ => e2e}/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml (100%) rename eng/tools/eslint-plugin-tsv/test/{ => e2e}/specification/eslint.config.ts (81%) diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index 61d6e6e955f9..03ad2eec9f9f 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -16,9 +16,9 @@ }, "scripts": { "build": "tsc --build", - "test:contoso": "eslint --config dist/test/specification/eslint.config.js test/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", - "test:not-kebab-case": "eslint --config dist/test/specification/eslint.config.js test/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", - "test:all": "npm run build; npm run test:contoso; npm run test:not-kebab-case" + "test:e2e": "npm run build && npm run test:e2e:contoso && npm run test:e2e:not-kebab-case", + "test:e2e:contoso": "eslint --config dist/test/specification/eslint.config.js test/e2e/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", + "test:e2e:not-kebab-case": "eslint --config dist/test/specification/eslint.config.js test/e2e/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml" }, "engines": { "node": ">= 18.0.0" diff --git a/eng/tools/eslint-plugin-tsv/test/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml b/eng/tools/eslint-plugin-tsv/test/e2e/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml similarity index 100% rename from eng/tools/eslint-plugin-tsv/test/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml rename to eng/tools/eslint-plugin-tsv/test/e2e/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml diff --git a/eng/tools/eslint-plugin-tsv/test/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml b/eng/tools/eslint-plugin-tsv/test/e2e/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml similarity index 100% rename from eng/tools/eslint-plugin-tsv/test/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml rename to eng/tools/eslint-plugin-tsv/test/e2e/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml diff --git a/eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts b/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts similarity index 81% rename from eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts rename to eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts index ca4f81f688f3..2c011ea0eb4c 100644 --- a/eng/tools/eslint-plugin-tsv/test/specification/eslint.config.ts +++ b/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts @@ -1,4 +1,4 @@ -import eslintPluginTsv from "../../src/eslint-plugin-tsv.js"; +import eslintPluginTsv from "../../../src/eslint-plugin-tsv.js"; import parser from "yaml-eslint-parser"; export const config = [ From 0fe7173f02749cfddce9688d4d65ca5db60c47fb Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Dec 2024 21:50:36 +0000 Subject: [PATCH 12/93] Add comment --- .../eslint-plugin-tsv/src/kebab-case-first-path-segment.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/eng/tools/eslint-plugin-tsv/src/kebab-case-first-path-segment.ts b/eng/tools/eslint-plugin-tsv/src/kebab-case-first-path-segment.ts index 7f5fb04b631e..1abc563e8a42 100644 --- a/eng/tools/eslint-plugin-tsv/src/kebab-case-first-path-segment.ts +++ b/eng/tools/eslint-plugin-tsv/src/kebab-case-first-path-segment.ts @@ -1,3 +1,5 @@ +// TODO: Add types + export const rule = { meta: { type: "problem", From da8c9aa3d09aa8eeb97355ec30530c717cbb9e3c Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Dec 2024 21:51:05 +0000 Subject: [PATCH 13/93] Add manual unit test --- eng/tools/eslint-plugin-tsv/package.json | 1 + .../test/kebab-case-first-path-segment.test.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 eng/tools/eslint-plugin-tsv/test/kebab-case-first-path-segment.test.ts diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index 03ad2eec9f9f..c308e3982f56 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -16,6 +16,7 @@ }, "scripts": { "build": "tsc --build", + "test":"node dist/test/kebab-case-first-path-segment.test.js", "test:e2e": "npm run build && npm run test:e2e:contoso && npm run test:e2e:not-kebab-case", "test:e2e:contoso": "eslint --config dist/test/specification/eslint.config.js test/e2e/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", "test:e2e:not-kebab-case": "eslint --config dist/test/specification/eslint.config.js test/e2e/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml" diff --git a/eng/tools/eslint-plugin-tsv/test/kebab-case-first-path-segment.test.ts b/eng/tools/eslint-plugin-tsv/test/kebab-case-first-path-segment.test.ts new file mode 100644 index 000000000000..a73f59f69e01 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/test/kebab-case-first-path-segment.test.ts @@ -0,0 +1,13 @@ +import { Rule, RuleTester } from "eslint"; +import kebabCaseFirstPathSegment from "../src/kebab-case-first-path-segment.js"; + +const ruleTester = new RuleTester(); + +ruleTester.run("kebab-case-first-path-segment", kebabCaseFirstPathSegment as Rule.RuleModule, { + valid: [{ code: "", filename: "/specification/contoso/Contoso.WidgetManager/tspconfig.yaml" }], + invalid: [ + { code: "", filename: "/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", errors: 1 }, + ], +}); + +console.log("All tests passed!"); From abf58cc4c3fb7b56eb5aa1a1cc45465395e49597 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Dec 2024 22:02:18 +0000 Subject: [PATCH 14/93] Convert test to vitest --- eng/tools/eslint-plugin-tsv/package.json | 6 ++-- .../src/eslint-plugin-tsv.ts | 3 +- .../src/kebab-case-first-path-segment.ts | 1 + .../kebab-case-first-path-segment.test.ts | 28 +++++++++++++------ 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index c308e3982f56..efa52f11f64c 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -12,11 +12,13 @@ "devDependencies": { "@types/node": "^18.19.31", "eslint": "^9.17.0", - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^2.0.4" }, "scripts": { "build": "tsc --build", - "test":"node dist/test/kebab-case-first-path-segment.test.js", + "test": "vitest", + "test:ci": "vitest run --reporter=verbose", "test:e2e": "npm run build && npm run test:e2e:contoso && npm run test:e2e:not-kebab-case", "test:e2e:contoso": "eslint --config dist/test/specification/eslint.config.js test/e2e/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", "test:e2e:not-kebab-case": "eslint --config dist/test/specification/eslint.config.js test/e2e/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml" diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts index c60b21834ec8..04b159227007 100644 --- a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts +++ b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -2,7 +2,8 @@ import kebabCaseFirstPathSegment from "./kebab-case-first-path-segment.js"; export const plugin = { rules: { - "kebab-case-first-path-segment": kebabCaseFirstPathSegment, + [kebabCaseFirstPathSegment.meta.name]: kebabCaseFirstPathSegment, }, }; + export default plugin; diff --git a/eng/tools/eslint-plugin-tsv/src/kebab-case-first-path-segment.ts b/eng/tools/eslint-plugin-tsv/src/kebab-case-first-path-segment.ts index 1abc563e8a42..c7cb4b2eb773 100644 --- a/eng/tools/eslint-plugin-tsv/src/kebab-case-first-path-segment.ts +++ b/eng/tools/eslint-plugin-tsv/src/kebab-case-first-path-segment.ts @@ -2,6 +2,7 @@ export const rule = { meta: { + name: "kebab-case-first-path-segment", type: "problem", docs: { description: "Requires first path segment after 'specification' to use kebab-case", diff --git a/eng/tools/eslint-plugin-tsv/test/kebab-case-first-path-segment.test.ts b/eng/tools/eslint-plugin-tsv/test/kebab-case-first-path-segment.test.ts index a73f59f69e01..d2899fc54b86 100644 --- a/eng/tools/eslint-plugin-tsv/test/kebab-case-first-path-segment.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/kebab-case-first-path-segment.test.ts @@ -1,13 +1,25 @@ import { Rule, RuleTester } from "eslint"; +import { test } from "vitest"; + import kebabCaseFirstPathSegment from "../src/kebab-case-first-path-segment.js"; -const ruleTester = new RuleTester(); +test(kebabCaseFirstPathSegment.meta.name, async ({ expect }) => { + const ruleTester = new RuleTester(); -ruleTester.run("kebab-case-first-path-segment", kebabCaseFirstPathSegment as Rule.RuleModule, { - valid: [{ code: "", filename: "/specification/contoso/Contoso.WidgetManager/tspconfig.yaml" }], - invalid: [ - { code: "", filename: "/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", errors: 1 }, - ], + ruleTester.run( + kebabCaseFirstPathSegment.meta.name, + kebabCaseFirstPathSegment as Rule.RuleModule, + { + valid: [ + { code: "", filename: "/specification/contoso/Contoso.WidgetManager/tspconfig.yaml" }, + ], + invalid: [ + { + code: "", + filename: "/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", + errors: 1, + }, + ], + }, + ); }); - -console.log("All tests passed!"); From 043d24e299d2adfa134cb3045e124f5b73099a7a Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 17 Dec 2024 22:10:02 +0000 Subject: [PATCH 15/93] Remove unnecessary async --- .../test/kebab-case-first-path-segment.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/tools/eslint-plugin-tsv/test/kebab-case-first-path-segment.test.ts b/eng/tools/eslint-plugin-tsv/test/kebab-case-first-path-segment.test.ts index d2899fc54b86..a8717f856c7e 100644 --- a/eng/tools/eslint-plugin-tsv/test/kebab-case-first-path-segment.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/kebab-case-first-path-segment.test.ts @@ -3,7 +3,7 @@ import { test } from "vitest"; import kebabCaseFirstPathSegment from "../src/kebab-case-first-path-segment.js"; -test(kebabCaseFirstPathSegment.meta.name, async ({ expect }) => { +test(kebabCaseFirstPathSegment.meta.name, () => { const ruleTester = new RuleTester(); ruleTester.run( From 6184c64a5618d7f228602d4ac9105d0f6e915de3 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Wed, 18 Dec 2024 01:18:58 +0000 Subject: [PATCH 16/93] Rename rule and improve error message --- eng/tools/eslint-plugin-tsv/package.json | 8 +- .../src/eslint-plugin-tsv.ts | 4 +- .../src/kebab-case-first-path-segment.ts | 36 ------ .../eslint-plugin-tsv/src/kebab-case-org.ts | 47 +++++++ .../test/e2e/specification/eslint.config.ts | 2 +- .../kebab-case-first-path-segment.test.ts | 25 ---- .../test/kebab-case-org.test.ts | 19 +++ package-lock.json | 117 +++++++++++++++++- 8 files changed, 190 insertions(+), 68 deletions(-) delete mode 100644 eng/tools/eslint-plugin-tsv/src/kebab-case-first-path-segment.ts create mode 100644 eng/tools/eslint-plugin-tsv/src/kebab-case-org.ts delete mode 100644 eng/tools/eslint-plugin-tsv/test/kebab-case-first-path-segment.test.ts create mode 100644 eng/tools/eslint-plugin-tsv/test/kebab-case-org.test.ts diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index efa52f11f64c..e2ecf79e6ab1 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -12,16 +12,18 @@ "devDependencies": { "@types/node": "^18.19.31", "eslint": "^9.17.0", + "rimraf": "^6.0.1", "typescript": "~5.6.2", "vitest": "^2.0.4" }, "scripts": { "build": "tsc --build", + "clean": "rimraf ./dist ./temp", "test": "vitest", "test:ci": "vitest run --reporter=verbose", - "test:e2e": "npm run build && npm run test:e2e:contoso && npm run test:e2e:not-kebab-case", - "test:e2e:contoso": "eslint --config dist/test/specification/eslint.config.js test/e2e/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", - "test:e2e:not-kebab-case": "eslint --config dist/test/specification/eslint.config.js test/e2e/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml" + "test:e2e": "npm run clean && npm run build && npm run test:e2e:contoso && npm run test:e2e:not-kebab-case", + "test:e2e:contoso": "eslint --config dist/test/e2e/specification/eslint.config.js test/e2e/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", + "test:e2e:not-kebab-case": "eslint --config dist/test/e2e/specification/eslint.config.js test/e2e/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml" }, "engines": { "node": ">= 18.0.0" diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts index 04b159227007..a9b684a2ca3a 100644 --- a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts +++ b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -1,8 +1,8 @@ -import kebabCaseFirstPathSegment from "./kebab-case-first-path-segment.js"; +import kebabCaseOrg from "./kebab-case-org.js"; export const plugin = { rules: { - [kebabCaseFirstPathSegment.meta.name]: kebabCaseFirstPathSegment, + [kebabCaseOrg.meta.name]: kebabCaseOrg, }, }; diff --git a/eng/tools/eslint-plugin-tsv/src/kebab-case-first-path-segment.ts b/eng/tools/eslint-plugin-tsv/src/kebab-case-first-path-segment.ts deleted file mode 100644 index c7cb4b2eb773..000000000000 --- a/eng/tools/eslint-plugin-tsv/src/kebab-case-first-path-segment.ts +++ /dev/null @@ -1,36 +0,0 @@ -// TODO: Add types - -export const rule = { - meta: { - name: "kebab-case-first-path-segment", - type: "problem", - docs: { - description: "Requires first path segment after 'specification' to use kebab-case", - }, - schema: [], - messages: { - kebab: "First path segment after 'specification' does not use kebab-case", - }, - }, - // @ts-ignore - create(context) { - const filename = context.getFilename() as string; - - const regex = /\/specification\/[a-z0-9]+(-[a-z0-9]+)*\//; - const kebabCaseFirstFolder = filename.match(regex); - - return { - // @ts-ignore - Program(node) { - if (!kebabCaseFirstFolder) { - context.report({ - node, - messageId: "kebab", - }); - } - }, - }; - }, -}; - -export default rule; diff --git a/eng/tools/eslint-plugin-tsv/src/kebab-case-org.ts b/eng/tools/eslint-plugin-tsv/src/kebab-case-org.ts new file mode 100644 index 000000000000..9801c4f8e337 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/kebab-case-org.ts @@ -0,0 +1,47 @@ +// TODO: Add types + +import path from "path"; + +export const rule = { + meta: { + name: "kebab-case-org", + type: "problem", + docs: { + description: + "Requires kebab-case for'organization' name (first path segment after 'specification')", + }, + schema: [], + messages: { + kebab: + "Organization name (first path segment after 'specification') does not use kebab-case: '{{orgName}}'", + }, + }, + // @ts-ignore + create(context) { + const filename = context.getFilename() as string; + + const pathSegments = filename.split(path.sep); + + // TODO: Handle errors + // - No "specification" segment + // - No segemnt after "specification" + const orgName = pathSegments[pathSegments.indexOf("specification") + 1]; + const kebabCaseRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/; + const orgNameKebabCase = orgName.match(kebabCaseRegex); + + return { + // @ts-ignore + Program(node) { + if (!orgNameKebabCase) { + context.report({ + node, + messageId: "kebab", + data: { orgName: orgName }, + }); + } + }, + }; + }, +}; + +export default rule; diff --git a/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts b/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts index 2c011ea0eb4c..449163503222 100644 --- a/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts +++ b/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts @@ -9,7 +9,7 @@ export const config = [ parser, }, rules: { - "tsv/kebab-case-first-path-segment": "error", + "tsv/kebab-case-org": "error", }, }, ]; diff --git a/eng/tools/eslint-plugin-tsv/test/kebab-case-first-path-segment.test.ts b/eng/tools/eslint-plugin-tsv/test/kebab-case-first-path-segment.test.ts deleted file mode 100644 index a8717f856c7e..000000000000 --- a/eng/tools/eslint-plugin-tsv/test/kebab-case-first-path-segment.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Rule, RuleTester } from "eslint"; -import { test } from "vitest"; - -import kebabCaseFirstPathSegment from "../src/kebab-case-first-path-segment.js"; - -test(kebabCaseFirstPathSegment.meta.name, () => { - const ruleTester = new RuleTester(); - - ruleTester.run( - kebabCaseFirstPathSegment.meta.name, - kebabCaseFirstPathSegment as Rule.RuleModule, - { - valid: [ - { code: "", filename: "/specification/contoso/Contoso.WidgetManager/tspconfig.yaml" }, - ], - invalid: [ - { - code: "", - filename: "/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", - errors: 1, - }, - ], - }, - ); -}); diff --git a/eng/tools/eslint-plugin-tsv/test/kebab-case-org.test.ts b/eng/tools/eslint-plugin-tsv/test/kebab-case-org.test.ts new file mode 100644 index 000000000000..313dd0cdfa0b --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/test/kebab-case-org.test.ts @@ -0,0 +1,19 @@ +import { Rule, RuleTester } from "eslint"; +import { test } from "vitest"; + +import kebabCaseOrg from "../src/kebab-case-org.js"; + +test(kebabCaseOrg.meta.name, () => { + const ruleTester = new RuleTester(); + + ruleTester.run(kebabCaseOrg.meta.name, kebabCaseOrg as Rule.RuleModule, { + valid: [{ code: "", filename: "/specification/contoso/Contoso.WidgetManager/tspconfig.yaml" }], + invalid: [ + { + code: "", + filename: "/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", + errors: 1, + }, + ], + }); +}); diff --git a/package-lock.json b/package-lock.json index f3328e3ac5ba..b625e0443439 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,9 @@ "devDependencies": { "@types/node": "^18.19.31", "eslint": "^9.17.0", - "typescript": "~5.6.2" + "rimraf": "^6.0.1", + "typescript": "~5.6.2", + "vitest": "^2.0.4" }, "engines": { "node": ">= 18.0.0" @@ -8558,6 +8560,119 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/jackspeak": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", From b03b6665042019c706faab1a78ddba223e0291f9 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Wed, 18 Dec 2024 01:30:09 +0000 Subject: [PATCH 17/93] Test eslint disablement comment --- .../eslint-plugin-tsv/test/kebab-case-org.test.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/test/kebab-case-org.test.ts b/eng/tools/eslint-plugin-tsv/test/kebab-case-org.test.ts index 313dd0cdfa0b..ddc68881f589 100644 --- a/eng/tools/eslint-plugin-tsv/test/kebab-case-org.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/kebab-case-org.test.ts @@ -1,13 +1,24 @@ import { Rule, RuleTester } from "eslint"; +import parser from "yaml-eslint-parser"; import { test } from "vitest"; import kebabCaseOrg from "../src/kebab-case-org.js"; test(kebabCaseOrg.meta.name, () => { - const ruleTester = new RuleTester(); + const ruleTester = new RuleTester({ + languageOptions: { + parser: parser, + }, + }); ruleTester.run(kebabCaseOrg.meta.name, kebabCaseOrg as Rule.RuleModule, { - valid: [{ code: "", filename: "/specification/contoso/Contoso.WidgetManager/tspconfig.yaml" }], + valid: [ + { code: "", filename: "/specification/contoso/Contoso.WidgetManager/tspconfig.yaml" }, + { + code: "# eslint-disable", + filename: "/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", + }, + ], invalid: [ { code: "", From 9c098940d9231cdb12f665769dd84a200b154f71 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Wed, 18 Dec 2024 02:04:09 +0000 Subject: [PATCH 18/93] Add tests for disabled rule --- eng/tools/eslint-plugin-tsv/package.json | 3 +- .../Not.KebabCase/tspconfig.yaml | 40 +++++++++++++++++++ .../test/kebab-case-org.test.ts | 6 +-- 3 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 eng/tools/eslint-plugin-tsv/test/e2e/specification/Not-Kebab-Case-Disabled/Not.KebabCase/tspconfig.yaml diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index e2ecf79e6ab1..d5c4240fbd69 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -21,8 +21,9 @@ "clean": "rimraf ./dist ./temp", "test": "vitest", "test:ci": "vitest run --reporter=verbose", - "test:e2e": "npm run clean && npm run build && npm run test:e2e:contoso && npm run test:e2e:not-kebab-case", + "test:e2e": "npm run clean && npm run build && npm run test:e2e:contoso && npm run test:e2e:not-kebab-case-disabled && npm run test:e2e:not-kebab-case", "test:e2e:contoso": "eslint --config dist/test/e2e/specification/eslint.config.js test/e2e/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", + "test:e2e:not-kebab-case-disabled": "eslint --config dist/test/e2e/specification/eslint.config.js test/e2e/specification/Not-Kebab-Case-Disabled/Not.KebabCase/tspconfig.yaml", "test:e2e:not-kebab-case": "eslint --config dist/test/e2e/specification/eslint.config.js test/e2e/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml" }, "engines": { diff --git a/eng/tools/eslint-plugin-tsv/test/e2e/specification/Not-Kebab-Case-Disabled/Not.KebabCase/tspconfig.yaml b/eng/tools/eslint-plugin-tsv/test/e2e/specification/Not-Kebab-Case-Disabled/Not.KebabCase/tspconfig.yaml new file mode 100644 index 000000000000..87dd5cf24346 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/test/e2e/specification/Not-Kebab-Case-Disabled/Not.KebabCase/tspconfig.yaml @@ -0,0 +1,40 @@ +# eslint-disable tsv/kebab-case-org +parameters: + "service-dir": + default: "sdk/contosowidgetmanager" + "dependencies": + "additionalDirectories": + - "specification/contosowidgetmanager/Contoso.WidgetManager.Shared/" + default: "" +emit: + - "@azure-tools/typespec-autorest" +linter: + extends: + - "@azure-tools/typespec-azure-rulesets/data-plane" +options: + "@azure-tools/typespec-autorest": + azure-resource-provider-folder: "data-plane" + emit-lro-options: "none" + emitter-output-dir: "{project-root}/.." + output-file: "{azure-resource-provider-folder}/{service-name}/{version-status}/{version}/widgets.json" + "@azure-tools/typespec-python": + package-dir: "azure-contoso-widgetmanager" + package-name: "{package-dir}" + generate-test: true + generate-sample: true + flavor: azure + "@azure-tools/typespec-csharp": + package-dir: "Azure.Template.Contoso" + clear-output-folder: true + model-namespace: false + namespace: "{package-dir}" + flavor: azure + "@azure-tools/typespec-ts": + package-dir: "contosowidgetmanager-rest" + packageDetails: + name: "@azure-rest/contoso-widgetmanager-rest" + flavor: azure + "@azure-tools/typespec-java": + package-dir: "azure-contoso-widgetmanager" + namespace: com.azure.contoso.widgetmanager + flavor: azure diff --git a/eng/tools/eslint-plugin-tsv/test/kebab-case-org.test.ts b/eng/tools/eslint-plugin-tsv/test/kebab-case-org.test.ts index ddc68881f589..fa484d729581 100644 --- a/eng/tools/eslint-plugin-tsv/test/kebab-case-org.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/kebab-case-org.test.ts @@ -4,7 +4,7 @@ import { test } from "vitest"; import kebabCaseOrg from "../src/kebab-case-org.js"; -test(kebabCaseOrg.meta.name, () => { +test("tsv/" + kebabCaseOrg.meta.name, () => { const ruleTester = new RuleTester({ languageOptions: { parser: parser, @@ -15,7 +15,7 @@ test(kebabCaseOrg.meta.name, () => { valid: [ { code: "", filename: "/specification/contoso/Contoso.WidgetManager/tspconfig.yaml" }, { - code: "# eslint-disable", + code: `# eslint-disable rule-to-test/${kebabCaseOrg.meta.name}`, filename: "/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", }, ], @@ -23,7 +23,7 @@ test(kebabCaseOrg.meta.name, () => { { code: "", filename: "/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", - errors: 1, + errors: ["Organization name (first path segment after 'specification') does not use kebab-case: 'Not-Kebab-Case'"], }, ], }); From a0d0fef8a5f21d78ab665e266c1e73e57a263c0d Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Wed, 18 Dec 2024 02:26:54 +0000 Subject: [PATCH 19/93] Include recommended config --- .../src/eslint-plugin-tsv.ts | 23 +++++++++++++++++++ .../test/e2e/specification/eslint.config.ts | 12 +--------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts index a9b684a2ca3a..16390fce9131 100644 --- a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts +++ b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -1,9 +1,32 @@ import kebabCaseOrg from "./kebab-case-org.js"; +import parser from "yaml-eslint-parser"; +const pluginName = "tsv"; + +// TODO: Add types export const plugin = { + configs: { }, rules: { [kebabCaseOrg.meta.name]: kebabCaseOrg, }, }; +// assign configs here so we can reference `plugin` +Object.assign(plugin.configs, { + recommended: [ + { + plugins: { + [pluginName]: plugin, + }, + files: ["*.yaml", "**/*.yaml"], + rules: { + [`${pluginName}/${kebabCaseOrg.meta.name}`]: "error", + }, + languageOptions: { + parser: parser, + }, + }, + ], +}); + export default plugin; diff --git a/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts b/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts index 449163503222..66462fb0d5f1 100644 --- a/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts +++ b/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts @@ -1,17 +1,7 @@ import eslintPluginTsv from "../../../src/eslint-plugin-tsv.js"; -import parser from "yaml-eslint-parser"; export const config = [ - { - plugins: { tsv: eslintPluginTsv }, - files: ["*.yaml", "**/*.yaml"], - languageOptions: { - parser, - }, - rules: { - "tsv/kebab-case-org": "error", - }, - }, + ...(eslintPluginTsv.configs as any).recommended, ]; export default config; From 0de1b3ee6a0c31f6517056fb813ffa18f3a370ef Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Wed, 18 Dec 2024 02:30:23 +0000 Subject: [PATCH 20/93] Move rules to subfolder --- eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts | 2 +- eng/tools/eslint-plugin-tsv/src/{ => rules}/kebab-case-org.ts | 0 .../eslint-plugin-tsv/test/{ => rules}/kebab-case-org.test.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename eng/tools/eslint-plugin-tsv/src/{ => rules}/kebab-case-org.ts (100%) rename eng/tools/eslint-plugin-tsv/test/{ => rules}/kebab-case-org.test.ts (93%) diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts index 16390fce9131..1e8409a219b8 100644 --- a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts +++ b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -1,4 +1,4 @@ -import kebabCaseOrg from "./kebab-case-org.js"; +import kebabCaseOrg from "./rules/kebab-case-org.js"; import parser from "yaml-eslint-parser"; const pluginName = "tsv"; diff --git a/eng/tools/eslint-plugin-tsv/src/kebab-case-org.ts b/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts similarity index 100% rename from eng/tools/eslint-plugin-tsv/src/kebab-case-org.ts rename to eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts diff --git a/eng/tools/eslint-plugin-tsv/test/kebab-case-org.test.ts b/eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts similarity index 93% rename from eng/tools/eslint-plugin-tsv/test/kebab-case-org.test.ts rename to eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts index fa484d729581..fbfe7d3b415c 100644 --- a/eng/tools/eslint-plugin-tsv/test/kebab-case-org.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts @@ -2,7 +2,7 @@ import { Rule, RuleTester } from "eslint"; import parser from "yaml-eslint-parser"; import { test } from "vitest"; -import kebabCaseOrg from "../src/kebab-case-org.js"; +import kebabCaseOrg from "../../src/rules/kebab-case-org.js"; test("tsv/" + kebabCaseOrg.meta.name, () => { const ruleTester = new RuleTester({ From 2208b20858b43602035ecdc4c19d299d9324b200 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Wed, 18 Dec 2024 03:04:21 +0000 Subject: [PATCH 21/93] Cleanup ts-ignore --- eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts b/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts index 9801c4f8e337..fd5192997f93 100644 --- a/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts +++ b/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts @@ -1,5 +1,3 @@ -// TODO: Add types - import path from "path"; export const rule = { @@ -16,8 +14,7 @@ export const rule = { "Organization name (first path segment after 'specification') does not use kebab-case: '{{orgName}}'", }, }, - // @ts-ignore - create(context) { + create(context: any) { const filename = context.getFilename() as string; const pathSegments = filename.split(path.sep); @@ -30,8 +27,7 @@ export const rule = { const orgNameKebabCase = orgName.match(kebabCaseRegex); return { - // @ts-ignore - Program(node) { + Program(node: any) { if (!orgNameKebabCase) { context.report({ node, From 5b3571f3c6571a07dbfed009c8727b868a72e0b3 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Wed, 18 Dec 2024 03:05:40 +0000 Subject: [PATCH 22/93] Remove comment --- eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts index 1e8409a219b8..9a0600671c69 100644 --- a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts +++ b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -3,7 +3,6 @@ import parser from "yaml-eslint-parser"; const pluginName = "tsv"; -// TODO: Add types export const plugin = { configs: { }, rules: { From cc6d19c160687976fc6698a3e1e02a49f176361b Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Wed, 18 Dec 2024 22:15:21 +0000 Subject: [PATCH 23/93] WIP: Add Npm.prefix() helper method and tests --- eng/tools/eslint-plugin-tsv/package.json | 1 + eng/tools/eslint-plugin-tsv/src/utils/npm.ts | 9 +++++++++ .../eslint-plugin-tsv/test/utils/npm.test.ts | 16 ++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 eng/tools/eslint-plugin-tsv/src/utils/npm.ts create mode 100644 eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index d5c4240fbd69..b5451de0a5aa 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -12,6 +12,7 @@ "devDependencies": { "@types/node": "^18.19.31", "eslint": "^9.17.0", + "memfs":"^4.15.0", "rimraf": "^6.0.1", "typescript": "~5.6.2", "vitest": "^2.0.4" diff --git a/eng/tools/eslint-plugin-tsv/src/utils/npm.ts b/eng/tools/eslint-plugin-tsv/src/utils/npm.ts new file mode 100644 index 000000000000..62c5b333e1ce --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/utils/npm.ts @@ -0,0 +1,9 @@ +import { dirname } from "path"; + +export class Npm { + // Simulates `npm prefix` by finding the nearest parent directory containing `package.json` or `node_modules`. + // If neither exist in any parent directories, returns the directory containing the path itself. + static prefix(path: string): string { + return dirname(path); + } +} diff --git a/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts b/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts new file mode 100644 index 000000000000..88610ef159d1 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts @@ -0,0 +1,16 @@ +// import { beforeEach, test, vi } from "vitest"; +import { describe, expect, it } from "vitest"; + +import { Npm } from "../../src/utils/npm.js"; + +// vi.mock('fs') +describe("prefix", () => { + describe("returns current directory if no match", () => { + it.each([ + ["/tmp/foo/tspconfig.yaml", "/tmp/foo"], + ["/tmp/foo", "/tmp/foo"], + ])("%s", async (path, expected) => { + expect(Npm.prefix(path)).toBe(expected); + }); + }); +}); From 659ed1e9bd31292b003408ec760b296eab96160a Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Wed, 18 Dec 2024 23:07:21 +0000 Subject: [PATCH 24/93] Use memfs to test fs code --- eng/tools/eslint-plugin-tsv/src/utils/npm.ts | 17 ++- .../eslint-plugin-tsv/test/utils/npm.test.ts | 23 +++- package-lock.json | 118 ++++++++++++++++++ 3 files changed, 150 insertions(+), 8 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/src/utils/npm.ts b/eng/tools/eslint-plugin-tsv/src/utils/npm.ts index 62c5b333e1ce..711a05f2ef70 100644 --- a/eng/tools/eslint-plugin-tsv/src/utils/npm.ts +++ b/eng/tools/eslint-plugin-tsv/src/utils/npm.ts @@ -1,9 +1,20 @@ -import { dirname } from "path"; +import * as path from "path"; +import * as fs from "fs/promises"; export class Npm { // Simulates `npm prefix` by finding the nearest parent directory containing `package.json` or `node_modules`. // If neither exist in any parent directories, returns the directory containing the path itself. - static prefix(path: string): string { - return dirname(path); + static async prefix(filePath: string): Promise { + const stats = await fs.stat(filePath); + + let currentDirectory: string; + if (stats.isDirectory()) { + currentDirectory = path.resolve(filePath); + } + else { + currentDirectory = path.dirname(filePath); + } + + return currentDirectory; } } diff --git a/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts b/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts index 88610ef159d1..b506100fe40c 100644 --- a/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts @@ -1,16 +1,29 @@ -// import { beforeEach, test, vi } from "vitest"; -import { describe, expect, it } from "vitest"; - +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { vol } from "memfs"; import { Npm } from "../../src/utils/npm.js"; -// vi.mock('fs') +vi.mock("fs/promises", async () => { + const memfs = await import("memfs"); + return { + ...memfs.fs.promises, + }; +}); + describe("prefix", () => { + beforeEach(() => { + vol.reset(); + }); + describe("returns current directory if no match", () => { it.each([ ["/tmp/foo/tspconfig.yaml", "/tmp/foo"], ["/tmp/foo", "/tmp/foo"], ])("%s", async (path, expected) => { - expect(Npm.prefix(path)).toBe(expected); + vol.fromJSON({ + "/tmp/foo/tspconfig.yaml": "1", + }); + + expect(await Npm.prefix(path)).toBe(expected); }); }); }); diff --git a/package-lock.json b/package-lock.json index b625e0443439..41cfa9dfc7a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,6 +56,7 @@ "devDependencies": { "@types/node": "^18.19.31", "eslint": "^9.17.0", + "memfs": "^4.15.0", "rimraf": "^6.0.1", "typescript": "~5.6.2", "vitest": "^2.0.4" @@ -2574,6 +2575,63 @@ "jsep": "^0.4.0||^1.0.0" } }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.1.1.tgz", + "integrity": "sha512-osjeBqMJ2lb/j/M8NCPjs1ylqWIcTRTycIhVB5pt6LgzgeRSb0YRZ7j9RfA8wIUrsr/medIuhVyonXRZWLyfdw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.1", + "@jsonjoy.com/util": "^1.1.2", + "hyperdyperid": "^1.2.0", + "thingies": "^1.20.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", + "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -6183,6 +6241,16 @@ "dev": true, "license": "Unlicense" }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -7012,6 +7080,26 @@ "dev": true, "license": "MIT" }, + "node_modules/memfs": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.15.0.tgz", + "integrity": "sha512-q9MmZXd2rRWHS6GU3WEm3HyiXZyyoA1DqdOhEq0lxPBmKb5S7IAOwX0RgUCwJfqjelDCySa5h8ujOy24LqsWcw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.3.0", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -9348,6 +9436,19 @@ "dev": true, "license": "MIT" }, + "node_modules/thingies": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", + "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=10.18" + }, + "peerDependencies": { + "tslib": "^2" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -9429,6 +9530,23 @@ "dev": true, "license": "MIT" }, + "node_modules/tree-dump": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", + "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", From a097482925595b2eace597a94510b6adb1e36612 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 00:28:07 +0000 Subject: [PATCH 25/93] Add failing tests --- .../eslint-plugin-tsv/test/utils/npm.test.ts | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts b/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts index b506100fe40c..feeb86707e61 100644 --- a/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts @@ -16,11 +16,37 @@ describe("prefix", () => { describe("returns current directory if no match", () => { it.each([ - ["/tmp/foo/tspconfig.yaml", "/tmp/foo"], - ["/tmp/foo", "/tmp/foo"], + ["/foo/bar/tspconfig.yaml", "/foo/bar"], + ["/foo/bar", "/foo/bar"], ])("%s", async (path, expected) => { vol.fromJSON({ - "/tmp/foo/tspconfig.yaml": "1", + "/foo/bar/tspconfig.yaml": "", + }); + + expect(await Npm.prefix(path)).toBe(expected); + }); + }); + + describe("returns first match", () => { + it.each([ + ["/pj", "/pj"], + ["/pj/none", "/pj"], + ["/pj/none/none/none", "/pj"], + ["/pj/nm", "/pj/nm"], + ["/pj/nm/none", "/pj/nm"], + ["/pj/pj", "/pj/pj"], + ["/pj/nm/pj", "/pj/nm/pj"], + ["/pj/pj/nm", "/pj/pj/nm"], + ])("%s", async (path, expected) => { + vol.fromJSON({ + "/pj/package.json": "", + "/pj/none": null, + "/pj/none/none/none": null, + "/pj/nm/node_modules": null, + "/pj/nm/none": null, + "/pj/pj/package.json": "", + "/pj/nm/pj/package.json": "", + "/pj/pj/nm/node_modules": null, }); expect(await Npm.prefix(path)).toBe(expected); From 1afea81b4e7b057b8d792d22b6d6c069130cb6ba Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 00:40:52 +0000 Subject: [PATCH 26/93] Implement prefix() --- eng/tools/eslint-plugin-tsv/src/utils/npm.ts | 34 ++++++++++++++------ 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/src/utils/npm.ts b/eng/tools/eslint-plugin-tsv/src/utils/npm.ts index 711a05f2ef70..9af3dd86db81 100644 --- a/eng/tools/eslint-plugin-tsv/src/utils/npm.ts +++ b/eng/tools/eslint-plugin-tsv/src/utils/npm.ts @@ -1,20 +1,36 @@ -import * as path from "path"; -import * as fs from "fs/promises"; +import { dirname, join, resolve } from "path"; +import { stat, access } from "fs/promises"; export class Npm { // Simulates `npm prefix` by finding the nearest parent directory containing `package.json` or `node_modules`. // If neither exist in any parent directories, returns the directory containing the path itself. - static async prefix(filePath: string): Promise { - const stats = await fs.stat(filePath); + static async prefix(path: string): Promise { + const stats = await stat(path); - let currentDirectory: string; + let initialDir: string; if (stats.isDirectory()) { - currentDirectory = path.resolve(filePath); + initialDir = resolve(path); + } else { + initialDir = dirname(path); } - else { - currentDirectory = path.dirname(filePath); + + for ( + var currentDir = initialDir; + dirname(currentDir) != currentDir; + currentDir = dirname(currentDir) + ) { + try { + await access(join(currentDir, "package.json")); + return currentDir; + } catch {} + + try { + await access(join(currentDir, "node_modules")); + return currentDir; + } catch {} } - return currentDirectory; + // Neither found in an parent dir + return initialDir; } } From 67ddba439e37bf8175e6b6da7c9029c8dc56ee90 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 00:43:26 +0000 Subject: [PATCH 27/93] Add more bulk test commands --- eng/tools/eslint-plugin-tsv/package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index b5451de0a5aa..476d39e84144 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -22,10 +22,12 @@ "clean": "rimraf ./dist ./temp", "test": "vitest", "test:ci": "vitest run --reporter=verbose", - "test:e2e": "npm run clean && npm run build && npm run test:e2e:contoso && npm run test:e2e:not-kebab-case-disabled && npm run test:e2e:not-kebab-case", + "test:e2e": "npm run clean && npm run build && npm run test:e2e:all", + "test:e2e:all": "npm run test:e2e:contoso && npm run test:e2e:not-kebab-case-disabled && npm run test:e2e:not-kebab-case", "test:e2e:contoso": "eslint --config dist/test/e2e/specification/eslint.config.js test/e2e/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", "test:e2e:not-kebab-case-disabled": "eslint --config dist/test/e2e/specification/eslint.config.js test/e2e/specification/Not-Kebab-Case-Disabled/Not.KebabCase/tspconfig.yaml", - "test:e2e:not-kebab-case": "eslint --config dist/test/e2e/specification/eslint.config.js test/e2e/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml" + "test:e2e:not-kebab-case": "eslint --config dist/test/e2e/specification/eslint.config.js test/e2e/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", + "test:all": "npm run clean && npm run build && npm run test:ci && npm run test:e2e:all" }, "engines": { "node": ">= 18.0.0" From 0c239f5d75cd2a0b34406897c0a71bc87cd7aad0 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 01:07:16 +0000 Subject: [PATCH 28/93] Add test workflow --- .github/workflows/eslint-plugin-tsv-test.yaml | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/eslint-plugin-tsv-test.yaml diff --git a/.github/workflows/eslint-plugin-tsv-test.yaml b/.github/workflows/eslint-plugin-tsv-test.yaml new file mode 100644 index 000000000000..f6af33b35953 --- /dev/null +++ b/.github/workflows/eslint-plugin-tsv-test.yaml @@ -0,0 +1,24 @@ +name: ESLint Plugin for TypeSpec Validation - Test + +on: + push: + branches: + - main + - typespec-next + pull_request: + paths: + - package-lock.json + - package.json + - tsconfig.json + - .github/workflows/_reusable-eng-tools-test.yaml + - .github/workflows/eslint-plugin-tsv-test.yaml + - eng/tools/package.json + - eng/tools/tsconfig.json + - eng/tools/eslint-plugin-tsv/** + workflow_dispatch: + +jobs: + typespec-validation: + uses: ./.github/workflows/_reusable-eng-tools-test.yaml + with: + package: eslint-plugin-tsv From 6903a12bbaadb2c18f63d388aa5ce3f586c860b1 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 01:31:43 +0000 Subject: [PATCH 29/93] Downgrade rimraf to v5 for node18 compat --- eng/tools/eslint-plugin-tsv/package.json | 2 +- package-lock.json | 152 ++++++----------------- 2 files changed, 39 insertions(+), 115 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index 476d39e84144..731cc22a81f6 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -13,7 +13,7 @@ "@types/node": "^18.19.31", "eslint": "^9.17.0", "memfs":"^4.15.0", - "rimraf": "^6.0.1", + "rimraf": "^5.0.10", "typescript": "~5.6.2", "vitest": "^2.0.4" }, diff --git a/package-lock.json b/package-lock.json index 41cfa9dfc7a3..ab897d4ac67c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,7 +57,7 @@ "@types/node": "^18.19.31", "eslint": "^9.17.0", "memfs": "^4.15.0", - "rimraf": "^6.0.1", + "rimraf": "^5.0.10", "typescript": "~5.6.2", "vitest": "^2.0.4" }, @@ -121,6 +121,27 @@ "balanced-match": "^1.0.0" } }, + "eng/tools/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "eng/tools/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -137,6 +158,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "eng/tools/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "eng/tools/node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -8648,119 +8685,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", - "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^11.0.0", - "package-json-from-dist": "^1.0.0" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/jackspeak": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", - "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/lru-cache": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", - "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", From 8978c866809076496c55c7bf83883f80342e49cd Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 01:39:15 +0000 Subject: [PATCH 30/93] Add code coverage --- eng/tools/eslint-plugin-tsv/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index 731cc22a81f6..946631b2b9d5 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -11,6 +11,7 @@ }, "devDependencies": { "@types/node": "^18.19.31", + "@vitest/coverage-v8": "^2.0.4", "eslint": "^9.17.0", "memfs":"^4.15.0", "rimraf": "^5.0.10", @@ -21,7 +22,7 @@ "build": "tsc --build", "clean": "rimraf ./dist ./temp", "test": "vitest", - "test:ci": "vitest run --reporter=verbose", + "test:ci": "vitest run --coverage --reporter=verbose", "test:e2e": "npm run clean && npm run build && npm run test:e2e:all", "test:e2e:all": "npm run test:e2e:contoso && npm run test:e2e:not-kebab-case-disabled && npm run test:e2e:not-kebab-case", "test:e2e:contoso": "eslint --config dist/test/e2e/specification/eslint.config.js test/e2e/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", From 83d874cc0c5568e78f57311b78b2aaa3f81f006c Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 02:06:11 +0000 Subject: [PATCH 31/93] Normalize paths so tests pass on win32 --- eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts b/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts index feeb86707e61..fd8052491f80 100644 --- a/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { vol } from "memfs"; +import { normalize } from "path"; import { Npm } from "../../src/utils/npm.js"; vi.mock("fs/promises", async () => { @@ -49,7 +50,7 @@ describe("prefix", () => { "/pj/pj/nm/node_modules": null, }); - expect(await Npm.prefix(path)).toBe(expected); + expect(await Npm.prefix(path)).toBe(normalize(expected)); }); }); }); From 786140b5adb0b736a764001ad2233f38ede02df4 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 02:08:20 +0000 Subject: [PATCH 32/93] normalize expected value --- eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts b/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts index fd8052491f80..14499a509690 100644 --- a/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts @@ -24,7 +24,7 @@ describe("prefix", () => { "/foo/bar/tspconfig.yaml": "", }); - expect(await Npm.prefix(path)).toBe(expected); + expect(await Npm.prefix(path)).toBe(normalize(expected)); }); }); From 40c079688e52ad3532a65e6376e5ba9f06166cbf Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 06:29:24 +0000 Subject: [PATCH 33/93] Rename job --- .github/workflows/eslint-plugin-tsv-test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/eslint-plugin-tsv-test.yaml b/.github/workflows/eslint-plugin-tsv-test.yaml index f6af33b35953..fc85aa16bf8b 100644 --- a/.github/workflows/eslint-plugin-tsv-test.yaml +++ b/.github/workflows/eslint-plugin-tsv-test.yaml @@ -18,7 +18,7 @@ on: workflow_dispatch: jobs: - typespec-validation: + eslint-plugin-tsv: uses: ./.github/workflows/_reusable-eng-tools-test.yaml with: package: eslint-plugin-tsv From 908df37146267944b6a27ca9957cda941fabadb8 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 06:29:37 +0000 Subject: [PATCH 34/93] Add comment --- eng/tools/eslint-plugin-tsv/src/utils/npm.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/eng/tools/eslint-plugin-tsv/src/utils/npm.ts b/eng/tools/eslint-plugin-tsv/src/utils/npm.ts index 9af3dd86db81..520657a2dd02 100644 --- a/eng/tools/eslint-plugin-tsv/src/utils/npm.ts +++ b/eng/tools/eslint-plugin-tsv/src/utils/npm.ts @@ -1,6 +1,8 @@ import { dirname, join, resolve } from "path"; import { stat, access } from "fs/promises"; +// TODO: Add @types/eslint + export class Npm { // Simulates `npm prefix` by finding the nearest parent directory containing `package.json` or `node_modules`. // If neither exist in any parent directories, returns the directory containing the path itself. From 5fe0a1f3f59e90cbbb3fb35f1c2ac4077a080af6 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 06:45:18 +0000 Subject: [PATCH 35/93] Always return absolute path --- eng/tools/eslint-plugin-tsv/src/utils/npm.ts | 10 +++------- eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts | 6 +++--- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/src/utils/npm.ts b/eng/tools/eslint-plugin-tsv/src/utils/npm.ts index 520657a2dd02..b5f32e0e6aa8 100644 --- a/eng/tools/eslint-plugin-tsv/src/utils/npm.ts +++ b/eng/tools/eslint-plugin-tsv/src/utils/npm.ts @@ -6,15 +6,11 @@ import { stat, access } from "fs/promises"; export class Npm { // Simulates `npm prefix` by finding the nearest parent directory containing `package.json` or `node_modules`. // If neither exist in any parent directories, returns the directory containing the path itself. + // Always returns an absolute path. static async prefix(path: string): Promise { - const stats = await stat(path); + path = resolve(path); - let initialDir: string; - if (stats.isDirectory()) { - initialDir = resolve(path); - } else { - initialDir = dirname(path); - } + const initialDir = (await stat(path)).isDirectory() ? path : dirname(path); for ( var currentDir = initialDir; diff --git a/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts b/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts index 14499a509690..28e54924170a 100644 --- a/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { vol } from "memfs"; -import { normalize } from "path"; +import { resolve } from "path"; import { Npm } from "../../src/utils/npm.js"; vi.mock("fs/promises", async () => { @@ -24,7 +24,7 @@ describe("prefix", () => { "/foo/bar/tspconfig.yaml": "", }); - expect(await Npm.prefix(path)).toBe(normalize(expected)); + expect(await Npm.prefix(path)).toBe(resolve(expected)); }); }); @@ -50,7 +50,7 @@ describe("prefix", () => { "/pj/pj/nm/node_modules": null, }); - expect(await Npm.prefix(path)).toBe(normalize(expected)); + expect(await Npm.prefix(path)).toBe(resolve(expected)); }); }); }); From 30093111de4f982ad935c1a545091ed6f9a0087e Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 09:19:42 +0000 Subject: [PATCH 36/93] Resolve path before splitting --- eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts b/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts index fd5192997f93..edbb866d3308 100644 --- a/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts +++ b/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts @@ -15,7 +15,7 @@ export const rule = { }, }, create(context: any) { - const filename = context.getFilename() as string; + const filename = path.resolve(context.filename as string); const pathSegments = filename.split(path.sep); From a92698f3f320d653064a4b580dd48efcac61de5d Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 22:24:18 +0000 Subject: [PATCH 37/93] Add stub for e2e vite tests --- eng/tools/eslint-plugin-tsv/test/e2e/e2e.test.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 eng/tools/eslint-plugin-tsv/test/e2e/e2e.test.ts diff --git a/eng/tools/eslint-plugin-tsv/test/e2e/e2e.test.ts b/eng/tools/eslint-plugin-tsv/test/e2e/e2e.test.ts new file mode 100644 index 000000000000..381cc910950b --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/test/e2e/e2e.test.ts @@ -0,0 +1 @@ +// TODO: Convert e2e tests to vitest \ No newline at end of file From 3e93efae4ed186091921ad0c75b66954a03d49f2 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 22:38:45 +0000 Subject: [PATCH 38/93] Remove comment --- eng/tools/eslint-plugin-tsv/src/utils/npm.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/src/utils/npm.ts b/eng/tools/eslint-plugin-tsv/src/utils/npm.ts index b5f32e0e6aa8..0d36838a1f8b 100644 --- a/eng/tools/eslint-plugin-tsv/src/utils/npm.ts +++ b/eng/tools/eslint-plugin-tsv/src/utils/npm.ts @@ -1,8 +1,6 @@ import { dirname, join, resolve } from "path"; import { stat, access } from "fs/promises"; -// TODO: Add @types/eslint - export class Npm { // Simulates `npm prefix` by finding the nearest parent directory containing `package.json` or `node_modules`. // If neither exist in any parent directories, returns the directory containing the path itself. From 41814f9292fafa2822f404d020173a68987680e3 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 22:39:20 +0000 Subject: [PATCH 39/93] Add ESLint with names --- .../src/eslint-plugin-tsv.ts | 43 +++++++++---------- .../eslint-plugin-tsv/src/named-eslint.ts | 16 +++++++ .../src/rules/kebab-case-org.ts | 9 ++-- .../test/e2e/specification/eslint.config.ts | 2 +- .../test/rules/kebab-case-org.test.ts | 12 +++--- 5 files changed, 50 insertions(+), 32 deletions(-) create mode 100644 eng/tools/eslint-plugin-tsv/src/named-eslint.ts diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts index 9a0600671c69..d3f241a25b17 100644 --- a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts +++ b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -1,31 +1,30 @@ -import kebabCaseOrg from "./rules/kebab-case-org.js"; +import { Linter } from "eslint"; import parser from "yaml-eslint-parser"; +import { NamedESLint } from "./named-eslint.js"; +import kebabCaseOrg from "./rules/kebab-case-org.js"; -const pluginName = "tsv"; - -export const plugin = { - configs: { }, +const plugin: NamedESLint.Plugin = { + name: "tsv", rules: { - [kebabCaseOrg.meta.name]: kebabCaseOrg, + [kebabCaseOrg.name]: kebabCaseOrg, }, }; -// assign configs here so we can reference `plugin` -Object.assign(plugin.configs, { - recommended: [ - { - plugins: { - [pluginName]: plugin, - }, - files: ["*.yaml", "**/*.yaml"], - rules: { - [`${pluginName}/${kebabCaseOrg.meta.name}`]: "error", - }, - languageOptions: { - parser: parser, - }, +const configs: Record = { + recommended: { + plugins: { + [plugin.name]: plugin, + }, + files: ["*.yaml", "**/*.yaml"], + rules: { + [`${plugin.name}/${kebabCaseOrg.name}`]: "error", }, - ], -}); + languageOptions: { + parser: parser, + }, + }, +}; + +plugin.configs = configs; export default plugin; diff --git a/eng/tools/eslint-plugin-tsv/src/named-eslint.ts b/eng/tools/eslint-plugin-tsv/src/named-eslint.ts new file mode 100644 index 000000000000..3b51d37656f2 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/named-eslint.ts @@ -0,0 +1,16 @@ +import { ESLint, Rule } from "eslint"; + +// ESLint with names for convenience + +export namespace NamedRule { + export interface RuleModule extends Rule.RuleModule { + name: string; + } +} + +export namespace NamedESLint { + export interface Plugin extends ESLint.Plugin { + name: string; + rules?: Record; + } +} diff --git a/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts b/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts index edbb866d3308..e5165dfc6b85 100644 --- a/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts +++ b/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts @@ -1,8 +1,9 @@ import path from "path"; +import { NamedRule } from "../named-eslint.js"; -export const rule = { +export const rule: NamedRule.RuleModule = { + name: "kebab-case-org", meta: { - name: "kebab-case-org", type: "problem", docs: { description: @@ -14,7 +15,7 @@ export const rule = { "Organization name (first path segment after 'specification') does not use kebab-case: '{{orgName}}'", }, }, - create(context: any) { + create(context) { const filename = path.resolve(context.filename as string); const pathSegments = filename.split(path.sep); @@ -27,7 +28,7 @@ export const rule = { const orgNameKebabCase = orgName.match(kebabCaseRegex); return { - Program(node: any) { + Program(node) { if (!orgNameKebabCase) { context.report({ node, diff --git a/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts b/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts index 66462fb0d5f1..c1d84deb1966 100644 --- a/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts +++ b/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts @@ -1,7 +1,7 @@ import eslintPluginTsv from "../../../src/eslint-plugin-tsv.js"; export const config = [ - ...(eslintPluginTsv.configs as any).recommended, + ...eslintPluginTsv.configs.recommended, ]; export default config; diff --git a/eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts b/eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts index fbfe7d3b415c..bb43e0b61abf 100644 --- a/eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts @@ -1,21 +1,21 @@ import { Rule, RuleTester } from "eslint"; -import parser from "yaml-eslint-parser"; import { test } from "vitest"; +import parser from "yaml-eslint-parser"; import kebabCaseOrg from "../../src/rules/kebab-case-org.js"; -test("tsv/" + kebabCaseOrg.meta.name, () => { +test("tsv/" + kebabCaseOrg.name, () => { const ruleTester = new RuleTester({ languageOptions: { parser: parser, }, }); - ruleTester.run(kebabCaseOrg.meta.name, kebabCaseOrg as Rule.RuleModule, { + ruleTester.run(kebabCaseOrg.name, kebabCaseOrg as Rule.RuleModule, { valid: [ { code: "", filename: "/specification/contoso/Contoso.WidgetManager/tspconfig.yaml" }, { - code: `# eslint-disable rule-to-test/${kebabCaseOrg.meta.name}`, + code: `# eslint-disable rule-to-test/${kebabCaseOrg.name}`, filename: "/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", }, ], @@ -23,7 +23,9 @@ test("tsv/" + kebabCaseOrg.meta.name, () => { { code: "", filename: "/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", - errors: ["Organization name (first path segment after 'specification') does not use kebab-case: 'Not-Kebab-Case'"], + errors: [ + "Organization name (first path segment after 'specification') does not use kebab-case: 'Not-Kebab-Case'", + ], }, ], }); From 5907a2100a5fa694d7289ea12d8869b9cfa65553 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 23:16:00 +0000 Subject: [PATCH 40/93] Add dummy test to prevent errors --- eng/tools/eslint-plugin-tsv/test/e2e/e2e.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/eng/tools/eslint-plugin-tsv/test/e2e/e2e.test.ts b/eng/tools/eslint-plugin-tsv/test/e2e/e2e.test.ts index 381cc910950b..3219a3a8d8f1 100644 --- a/eng/tools/eslint-plugin-tsv/test/e2e/e2e.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/e2e/e2e.test.ts @@ -1 +1,7 @@ -// TODO: Convert e2e tests to vitest \ No newline at end of file +import { describe, it } from "vitest"; + +// TODO: Convert e2e tests to vitest + +describe("e2e", () => { + it("passes", () => true); +}); From d0ae1deaef2a6da34a767956a73f983a8eddfaf7 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 23:16:35 +0000 Subject: [PATCH 41/93] Stronger config typing --- .../src/eslint-plugin-tsv.ts | 26 ++++++++----------- .../eslint-plugin-tsv/src/named-eslint.ts | 3 ++- .../test/e2e/specification/eslint.config.ts | 6 +---- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts index d3f241a25b17..bdac409b2574 100644 --- a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts +++ b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -1,30 +1,26 @@ -import { Linter } from "eslint"; import parser from "yaml-eslint-parser"; import { NamedESLint } from "./named-eslint.js"; import kebabCaseOrg from "./rules/kebab-case-org.js"; const plugin: NamedESLint.Plugin = { + configs: { recommended: {} }, name: "tsv", rules: { [kebabCaseOrg.name]: kebabCaseOrg, }, }; -const configs: Record = { - recommended: { - plugins: { - [plugin.name]: plugin, - }, - files: ["*.yaml", "**/*.yaml"], - rules: { - [`${plugin.name}/${kebabCaseOrg.name}`]: "error", - }, - languageOptions: { - parser: parser, - }, +plugin.configs.recommended = { + plugins: { + [plugin.name]: plugin, + }, + files: ["*.yaml", "**/*.yaml"], + rules: { + [`${plugin.name}/${kebabCaseOrg.name}`]: "error", + }, + languageOptions: { + parser: parser, }, }; -plugin.configs = configs; - export default plugin; diff --git a/eng/tools/eslint-plugin-tsv/src/named-eslint.ts b/eng/tools/eslint-plugin-tsv/src/named-eslint.ts index 3b51d37656f2..a09d8d27f150 100644 --- a/eng/tools/eslint-plugin-tsv/src/named-eslint.ts +++ b/eng/tools/eslint-plugin-tsv/src/named-eslint.ts @@ -1,4 +1,4 @@ -import { ESLint, Rule } from "eslint"; +import { ESLint, Linter, Rule } from "eslint"; // ESLint with names for convenience @@ -10,6 +10,7 @@ export namespace NamedRule { export namespace NamedESLint { export interface Plugin extends ESLint.Plugin { + configs: { recommended: Linter.Config }; name: string; rules?: Record; } diff --git a/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts b/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts index c1d84deb1966..a62b6ebf16b2 100644 --- a/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts +++ b/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts @@ -1,7 +1,3 @@ import eslintPluginTsv from "../../../src/eslint-plugin-tsv.js"; -export const config = [ - ...eslintPluginTsv.configs.recommended, -]; - -export default config; +export default eslintPluginTsv.configs.recommended; From 49d09b681aa117b7d5e912479cb70ea333565719 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Thu, 19 Dec 2024 23:51:06 +0000 Subject: [PATCH 42/93] Move e2e test --- eng/tools/eslint-plugin-tsv/test/e2e.test.ts | 12 ++++++++++++ eng/tools/eslint-plugin-tsv/test/e2e/e2e.test.ts | 7 ------- 2 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 eng/tools/eslint-plugin-tsv/test/e2e.test.ts delete mode 100644 eng/tools/eslint-plugin-tsv/test/e2e/e2e.test.ts diff --git a/eng/tools/eslint-plugin-tsv/test/e2e.test.ts b/eng/tools/eslint-plugin-tsv/test/e2e.test.ts new file mode 100644 index 000000000000..ee6ab9a40fac --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/test/e2e.test.ts @@ -0,0 +1,12 @@ +import { ESLint } from "eslint"; +import { describe, expect, it } from "vitest"; + +// TODO: Convert e2e tests to vitest + +describe("e2e", () => { + it("/dev/null", async () => { + const eslint = new ESLint(); + const results = await eslint.lintFiles("/dev/null"); + expect(results).toHaveLength(0); + }); +}); diff --git a/eng/tools/eslint-plugin-tsv/test/e2e/e2e.test.ts b/eng/tools/eslint-plugin-tsv/test/e2e/e2e.test.ts deleted file mode 100644 index 3219a3a8d8f1..000000000000 --- a/eng/tools/eslint-plugin-tsv/test/e2e/e2e.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, it } from "vitest"; - -// TODO: Convert e2e tests to vitest - -describe("e2e", () => { - it("passes", () => true); -}); From 2563cd236049bf898121c3c65a7b001edb8a752c Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Fri, 20 Dec 2024 00:38:22 +0000 Subject: [PATCH 43/93] Run tests when contoso changes --- .github/workflows/eslint-plugin-tsv-test.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/eslint-plugin-tsv-test.yaml b/.github/workflows/eslint-plugin-tsv-test.yaml index fc85aa16bf8b..9fcdc7b47f46 100644 --- a/.github/workflows/eslint-plugin-tsv-test.yaml +++ b/.github/workflows/eslint-plugin-tsv-test.yaml @@ -15,6 +15,7 @@ on: - eng/tools/package.json - eng/tools/tsconfig.json - eng/tools/eslint-plugin-tsv/** + - specification/contosowidgetmanager workflow_dispatch: jobs: From 6a555fb1fc8588dfc252f030393550a53e2c7e1c Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Fri, 20 Dec 2024 01:36:54 +0000 Subject: [PATCH 44/93] Add e2e test using real filesystem --- .../eslint-plugin-tsv/test/e2e-realfs.test.ts | 17 ++++++++ .../Contoso.WidgetManager/tspconfig.yaml | 39 ------------------- eng/tools/eslint-plugin-tsv/test/utils/e2e.ts | 10 +++++ 3 files changed, 27 insertions(+), 39 deletions(-) create mode 100644 eng/tools/eslint-plugin-tsv/test/e2e-realfs.test.ts delete mode 100644 eng/tools/eslint-plugin-tsv/test/e2e/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml create mode 100644 eng/tools/eslint-plugin-tsv/test/utils/e2e.ts diff --git a/eng/tools/eslint-plugin-tsv/test/e2e-realfs.test.ts b/eng/tools/eslint-plugin-tsv/test/e2e-realfs.test.ts new file mode 100644 index 000000000000..29fc83476f3a --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/test/e2e-realfs.test.ts @@ -0,0 +1,17 @@ +import { join, resolve } from "path"; +import { describe, expect, it } from "vitest"; +import { createESLint } from "./utils/e2e.js"; + +const specsFolder = resolve(__filename, "../../../../../specification"); + +describe("e2e-realfs", () => { + it("contosowidgetmanager/Contso.WidgetManager", async () => { + const eslint = createESLint(); + const filePath = join(specsFolder, "contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml"); + const results = await eslint.lintFiles(filePath); + + expect(results).toHaveLength(1); + expect(results[0].filePath).toBe(filePath); + expect(results[0].messages).toHaveLength(0); + }); +}); diff --git a/eng/tools/eslint-plugin-tsv/test/e2e/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml b/eng/tools/eslint-plugin-tsv/test/e2e/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml deleted file mode 100644 index 2633b3a76f34..000000000000 --- a/eng/tools/eslint-plugin-tsv/test/e2e/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml +++ /dev/null @@ -1,39 +0,0 @@ -parameters: - "service-dir": - default: "sdk/contosowidgetmanager" - "dependencies": - "additionalDirectories": - - "specification/contosowidgetmanager/Contoso.WidgetManager.Shared/" - default: "" -emit: - - "@azure-tools/typespec-autorest" -linter: - extends: - - "@azure-tools/typespec-azure-rulesets/data-plane" -options: - "@azure-tools/typespec-autorest": - azure-resource-provider-folder: "data-plane" - emit-lro-options: "none" - emitter-output-dir: "{project-root}/.." - output-file: "{azure-resource-provider-folder}/{service-name}/{version-status}/{version}/widgets.json" - "@azure-tools/typespec-python": - package-dir: "azure-contoso-widgetmanager" - package-name: "{package-dir}" - generate-test: true - generate-sample: true - flavor: azure - "@azure-tools/typespec-csharp": - package-dir: "Azure.Template.Contoso" - clear-output-folder: true - model-namespace: false - namespace: "{package-dir}" - flavor: azure - "@azure-tools/typespec-ts": - package-dir: "contosowidgetmanager-rest" - packageDetails: - name: "@azure-rest/contoso-widgetmanager-rest" - flavor: azure - "@azure-tools/typespec-java": - package-dir: "azure-contoso-widgetmanager" - namespace: com.azure.contoso.widgetmanager - flavor: azure diff --git a/eng/tools/eslint-plugin-tsv/test/utils/e2e.ts b/eng/tools/eslint-plugin-tsv/test/utils/e2e.ts new file mode 100644 index 000000000000..5a9ff9e14054 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/test/utils/e2e.ts @@ -0,0 +1,10 @@ +import { ESLint } from "eslint"; +import eslintPluginTsv from "../../src/eslint-plugin-tsv.js"; + +export function createESLint() { + return new ESLint({ + cwd: "/", + overrideConfig: eslintPluginTsv.configs.recommended, + overrideConfigFile: true, + }); +} From dd472f891381b27d952f04d992e549eafd02efc5 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Fri, 20 Dec 2024 01:37:31 +0000 Subject: [PATCH 45/93] Remove old test --- eng/tools/eslint-plugin-tsv/test/e2e.test.ts | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 eng/tools/eslint-plugin-tsv/test/e2e.test.ts diff --git a/eng/tools/eslint-plugin-tsv/test/e2e.test.ts b/eng/tools/eslint-plugin-tsv/test/e2e.test.ts deleted file mode 100644 index ee6ab9a40fac..000000000000 --- a/eng/tools/eslint-plugin-tsv/test/e2e.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ESLint } from "eslint"; -import { describe, expect, it } from "vitest"; - -// TODO: Convert e2e tests to vitest - -describe("e2e", () => { - it("/dev/null", async () => { - const eslint = new ESLint(); - const results = await eslint.lintFiles("/dev/null"); - expect(results).toHaveLength(0); - }); -}); From d7439da21cc0dede4f0fd9d6312da4a942cd2408 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Fri, 20 Dec 2024 02:18:33 +0000 Subject: [PATCH 46/93] Improve test name --- eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts b/eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts index bb43e0b61abf..dc9162122aea 100644 --- a/eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts @@ -4,7 +4,7 @@ import parser from "yaml-eslint-parser"; import kebabCaseOrg from "../../src/rules/kebab-case-org.js"; -test("tsv/" + kebabCaseOrg.name, () => { +test("RuleTester", () => { const ruleTester = new RuleTester({ languageOptions: { parser: parser, From 90fab866cd84cde5f42638d13c6ea646164bb154 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Fri, 20 Dec 2024 02:19:30 +0000 Subject: [PATCH 47/93] Fix e2e tests --- .../eslint-plugin-tsv/test/e2e-realfs.test.ts | 17 ------ eng/tools/eslint-plugin-tsv/test/e2e.test.ts | 52 +++++++++++++++++++ .../Not.KebabCase/tspconfig.yaml | 40 -------------- .../Not.KebabCase/tspconfig.yaml | 39 -------------- .../test/e2e/specification/eslint.config.ts | 3 -- eng/tools/eslint-plugin-tsv/test/utils/e2e.ts | 10 ---- 6 files changed, 52 insertions(+), 109 deletions(-) delete mode 100644 eng/tools/eslint-plugin-tsv/test/e2e-realfs.test.ts create mode 100644 eng/tools/eslint-plugin-tsv/test/e2e.test.ts delete mode 100644 eng/tools/eslint-plugin-tsv/test/e2e/specification/Not-Kebab-Case-Disabled/Not.KebabCase/tspconfig.yaml delete mode 100644 eng/tools/eslint-plugin-tsv/test/e2e/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml delete mode 100644 eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts delete mode 100644 eng/tools/eslint-plugin-tsv/test/utils/e2e.ts diff --git a/eng/tools/eslint-plugin-tsv/test/e2e-realfs.test.ts b/eng/tools/eslint-plugin-tsv/test/e2e-realfs.test.ts deleted file mode 100644 index 29fc83476f3a..000000000000 --- a/eng/tools/eslint-plugin-tsv/test/e2e-realfs.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { join, resolve } from "path"; -import { describe, expect, it } from "vitest"; -import { createESLint } from "./utils/e2e.js"; - -const specsFolder = resolve(__filename, "../../../../../specification"); - -describe("e2e-realfs", () => { - it("contosowidgetmanager/Contso.WidgetManager", async () => { - const eslint = createESLint(); - const filePath = join(specsFolder, "contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml"); - const results = await eslint.lintFiles(filePath); - - expect(results).toHaveLength(1); - expect(results[0].filePath).toBe(filePath); - expect(results[0].messages).toHaveLength(0); - }); -}); diff --git a/eng/tools/eslint-plugin-tsv/test/e2e.test.ts b/eng/tools/eslint-plugin-tsv/test/e2e.test.ts new file mode 100644 index 000000000000..33bd6daf518b --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/test/e2e.test.ts @@ -0,0 +1,52 @@ +import { ESLint } from "eslint"; +import { join, resolve } from "path"; +import { describe, expect, it } from "vitest"; +import eslintPluginTsv from "../src/eslint-plugin-tsv.js"; + +function createESLint() { + return new ESLint({ + cwd: "/", + overrideConfig: eslintPluginTsv.configs.recommended, + overrideConfigFile: true, + }); +} + +describe("lint-text", () => { + it("Not-Kebab-Case/Not.KebabCase", async () => { + const filePath = "/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml"; + const eslint = createESLint(); + + const results = await eslint.lintText("", { filePath: filePath }); + + expect(results).toHaveLength(1); + expect(results[0].filePath).toBe(filePath); + expect(results[0].messages[0].ruleId).toBe("tsv/kebab-case-org"); + }); + + it("Not-Kebab-Case-Disabled/Not.KebabCase", async () => { + const filePath = "/specification/Not-Kebab-Case-Disabled/Not.KebabCase/tspconfig.yaml"; + const eslint = createESLint(); + + const results = await eslint.lintText("# eslint-disable tsv/kebab-case-org", { + filePath: filePath, + }); + + expect(results).toHaveLength(1); + expect(results[0].filePath).toBe(filePath); + expect(results[0].messages).toHaveLength(0); + }); +}); + +describe("lint-files", () => { + const specsFolder = resolve(__filename, "../../../../../specification"); + + it("contosowidgetmanager/Contso.WidgetManager", async () => { + const eslint = createESLint(); + const filePath = join(specsFolder, "contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml"); + const results = await eslint.lintFiles(filePath); + + expect(results).toHaveLength(1); + expect(results[0].filePath).toBe(filePath); + expect(results[0].messages).toHaveLength(0); + }); +}); diff --git a/eng/tools/eslint-plugin-tsv/test/e2e/specification/Not-Kebab-Case-Disabled/Not.KebabCase/tspconfig.yaml b/eng/tools/eslint-plugin-tsv/test/e2e/specification/Not-Kebab-Case-Disabled/Not.KebabCase/tspconfig.yaml deleted file mode 100644 index 87dd5cf24346..000000000000 --- a/eng/tools/eslint-plugin-tsv/test/e2e/specification/Not-Kebab-Case-Disabled/Not.KebabCase/tspconfig.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# eslint-disable tsv/kebab-case-org -parameters: - "service-dir": - default: "sdk/contosowidgetmanager" - "dependencies": - "additionalDirectories": - - "specification/contosowidgetmanager/Contoso.WidgetManager.Shared/" - default: "" -emit: - - "@azure-tools/typespec-autorest" -linter: - extends: - - "@azure-tools/typespec-azure-rulesets/data-plane" -options: - "@azure-tools/typespec-autorest": - azure-resource-provider-folder: "data-plane" - emit-lro-options: "none" - emitter-output-dir: "{project-root}/.." - output-file: "{azure-resource-provider-folder}/{service-name}/{version-status}/{version}/widgets.json" - "@azure-tools/typespec-python": - package-dir: "azure-contoso-widgetmanager" - package-name: "{package-dir}" - generate-test: true - generate-sample: true - flavor: azure - "@azure-tools/typespec-csharp": - package-dir: "Azure.Template.Contoso" - clear-output-folder: true - model-namespace: false - namespace: "{package-dir}" - flavor: azure - "@azure-tools/typespec-ts": - package-dir: "contosowidgetmanager-rest" - packageDetails: - name: "@azure-rest/contoso-widgetmanager-rest" - flavor: azure - "@azure-tools/typespec-java": - package-dir: "azure-contoso-widgetmanager" - namespace: com.azure.contoso.widgetmanager - flavor: azure diff --git a/eng/tools/eslint-plugin-tsv/test/e2e/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml b/eng/tools/eslint-plugin-tsv/test/e2e/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml deleted file mode 100644 index 2633b3a76f34..000000000000 --- a/eng/tools/eslint-plugin-tsv/test/e2e/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml +++ /dev/null @@ -1,39 +0,0 @@ -parameters: - "service-dir": - default: "sdk/contosowidgetmanager" - "dependencies": - "additionalDirectories": - - "specification/contosowidgetmanager/Contoso.WidgetManager.Shared/" - default: "" -emit: - - "@azure-tools/typespec-autorest" -linter: - extends: - - "@azure-tools/typespec-azure-rulesets/data-plane" -options: - "@azure-tools/typespec-autorest": - azure-resource-provider-folder: "data-plane" - emit-lro-options: "none" - emitter-output-dir: "{project-root}/.." - output-file: "{azure-resource-provider-folder}/{service-name}/{version-status}/{version}/widgets.json" - "@azure-tools/typespec-python": - package-dir: "azure-contoso-widgetmanager" - package-name: "{package-dir}" - generate-test: true - generate-sample: true - flavor: azure - "@azure-tools/typespec-csharp": - package-dir: "Azure.Template.Contoso" - clear-output-folder: true - model-namespace: false - namespace: "{package-dir}" - flavor: azure - "@azure-tools/typespec-ts": - package-dir: "contosowidgetmanager-rest" - packageDetails: - name: "@azure-rest/contoso-widgetmanager-rest" - flavor: azure - "@azure-tools/typespec-java": - package-dir: "azure-contoso-widgetmanager" - namespace: com.azure.contoso.widgetmanager - flavor: azure diff --git a/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts b/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts deleted file mode 100644 index a62b6ebf16b2..000000000000 --- a/eng/tools/eslint-plugin-tsv/test/e2e/specification/eslint.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import eslintPluginTsv from "../../../src/eslint-plugin-tsv.js"; - -export default eslintPluginTsv.configs.recommended; diff --git a/eng/tools/eslint-plugin-tsv/test/utils/e2e.ts b/eng/tools/eslint-plugin-tsv/test/utils/e2e.ts deleted file mode 100644 index 5a9ff9e14054..000000000000 --- a/eng/tools/eslint-plugin-tsv/test/utils/e2e.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ESLint } from "eslint"; -import eslintPluginTsv from "../../src/eslint-plugin-tsv.js"; - -export function createESLint() { - return new ESLint({ - cwd: "/", - overrideConfig: eslintPluginTsv.configs.recommended, - overrideConfigFile: true, - }); -} From 9f5ad9b5b106124520cec7d1ff8c1b29d041b210 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Fri, 20 Dec 2024 02:56:12 +0000 Subject: [PATCH 48/93] Exclude interface from coverage --- eng/tools/eslint-plugin-tsv/vitest.config.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 eng/tools/eslint-plugin-tsv/vitest.config.ts diff --git a/eng/tools/eslint-plugin-tsv/vitest.config.ts b/eng/tools/eslint-plugin-tsv/vitest.config.ts new file mode 100644 index 000000000000..21187f0fc527 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + coverage: { + include: ["src"], + exclude: ["src/named-eslint.ts"], + }, + }, +}); From fac51d74ffd5ecb96dc5f1242909b0731f1772ac Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Fri, 20 Dec 2024 03:02:01 +0000 Subject: [PATCH 49/93] Exclude interfaces from coverage --- eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts | 2 +- .../eslint-plugin-tsv/src/{ => interfaces}/named-eslint.ts | 0 eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts | 2 +- eng/tools/eslint-plugin-tsv/vitest.config.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename eng/tools/eslint-plugin-tsv/src/{ => interfaces}/named-eslint.ts (100%) diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts index bdac409b2574..f620058f6048 100644 --- a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts +++ b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -1,5 +1,5 @@ import parser from "yaml-eslint-parser"; -import { NamedESLint } from "./named-eslint.js"; +import { NamedESLint } from "./interfaces/named-eslint.js"; import kebabCaseOrg from "./rules/kebab-case-org.js"; const plugin: NamedESLint.Plugin = { diff --git a/eng/tools/eslint-plugin-tsv/src/named-eslint.ts b/eng/tools/eslint-plugin-tsv/src/interfaces/named-eslint.ts similarity index 100% rename from eng/tools/eslint-plugin-tsv/src/named-eslint.ts rename to eng/tools/eslint-plugin-tsv/src/interfaces/named-eslint.ts diff --git a/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts b/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts index e5165dfc6b85..4b6e6468e0f1 100644 --- a/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts +++ b/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts @@ -1,5 +1,5 @@ import path from "path"; -import { NamedRule } from "../named-eslint.js"; +import { NamedRule } from "../interfaces/named-eslint.js"; export const rule: NamedRule.RuleModule = { name: "kebab-case-org", diff --git a/eng/tools/eslint-plugin-tsv/vitest.config.ts b/eng/tools/eslint-plugin-tsv/vitest.config.ts index 21187f0fc527..785acc9b7335 100644 --- a/eng/tools/eslint-plugin-tsv/vitest.config.ts +++ b/eng/tools/eslint-plugin-tsv/vitest.config.ts @@ -4,7 +4,7 @@ export default defineConfig({ test: { coverage: { include: ["src"], - exclude: ["src/named-eslint.ts"], + exclude: ["src/interfaces"], }, }, }); From 313f9269a3df6f14a6252e3cba1df7fa4ea6e8d9 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Fri, 20 Dec 2024 03:05:16 +0000 Subject: [PATCH 50/93] Add Management e2e test --- eng/tools/eslint-plugin-tsv/test/e2e.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/eng/tools/eslint-plugin-tsv/test/e2e.test.ts b/eng/tools/eslint-plugin-tsv/test/e2e.test.ts index 33bd6daf518b..9d143eae745e 100644 --- a/eng/tools/eslint-plugin-tsv/test/e2e.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/e2e.test.ts @@ -49,4 +49,14 @@ describe("lint-files", () => { expect(results[0].filePath).toBe(filePath); expect(results[0].messages).toHaveLength(0); }); + + it("contosowidgetmanager/Contso.Management", async () => { + const eslint = createESLint(); + const filePath = join(specsFolder, "contosowidgetmanager/Contoso.Management/tspconfig.yaml"); + const results = await eslint.lintFiles(filePath); + + expect(results).toHaveLength(1); + expect(results[0].filePath).toBe(filePath); + expect(results[0].messages).toHaveLength(0); + }); }); From 5cb8558c2251b45cfedda95f9ee348a6566346d3 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Fri, 20 Dec 2024 03:18:13 +0000 Subject: [PATCH 51/93] improve error validation --- eng/tools/eslint-plugin-tsv/test/e2e.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/eng/tools/eslint-plugin-tsv/test/e2e.test.ts b/eng/tools/eslint-plugin-tsv/test/e2e.test.ts index 9d143eae745e..5d28184c575a 100644 --- a/eng/tools/eslint-plugin-tsv/test/e2e.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/e2e.test.ts @@ -21,6 +21,7 @@ describe("lint-text", () => { expect(results).toHaveLength(1); expect(results[0].filePath).toBe(filePath); expect(results[0].messages[0].ruleId).toBe("tsv/kebab-case-org"); + expect(results[0].messages[0].messageId).toBe("kebab"); }); it("Not-Kebab-Case-Disabled/Not.KebabCase", async () => { From 29c9f1fc71f0289743c9dfa36838e1722c13fb7f Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Fri, 20 Dec 2024 03:18:25 +0000 Subject: [PATCH 52/93] Handle invalid path errors --- .../src/rules/kebab-case-org.ts | 28 ++++++++++++------- .../test/rules/kebab-case-org.test.ts | 9 ++++-- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts b/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts index 4b6e6468e0f1..ca1dc5df2397 100644 --- a/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts +++ b/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts @@ -11,24 +11,32 @@ export const rule: NamedRule.RuleModule = { }, schema: [], messages: { + invalid: "Path does not match format '.*/specification/{orgName}/': ''{{filename}}'", kebab: "Organization name (first path segment after 'specification') does not use kebab-case: '{{orgName}}'", }, }, create(context) { - const filename = path.resolve(context.filename as string); + return { + Program(node) { + const filename = path.resolve(context.filename as string); + const pathSegments = filename.split(path.sep); + const specificationIndex = pathSegments.indexOf("specification"); + const pathValid = specificationIndex >= 0 && specificationIndex < pathSegments.length - 1; - const pathSegments = filename.split(path.sep); + if (!pathValid) { + context.report({ + node, + messageId: "invalid", + data: { filename: filename }, + }); + return; + } - // TODO: Handle errors - // - No "specification" segment - // - No segemnt after "specification" - const orgName = pathSegments[pathSegments.indexOf("specification") + 1]; - const kebabCaseRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/; - const orgNameKebabCase = orgName.match(kebabCaseRegex); + const orgName = pathSegments[specificationIndex + 1]; + const kebabCaseRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/; + const orgNameKebabCase = orgName.match(kebabCaseRegex); - return { - Program(node) { if (!orgNameKebabCase) { context.report({ node, diff --git a/eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts b/eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts index dc9162122aea..b7a0092f3282 100644 --- a/eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts @@ -23,9 +23,12 @@ test("RuleTester", () => { { code: "", filename: "/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", - errors: [ - "Organization name (first path segment after 'specification') does not use kebab-case: 'Not-Kebab-Case'", - ], + errors: [{ messageId: "kebab" }], + }, + { + code: "", + filename: "tspconfig.yaml", + errors: [{ messageId: "invalid" }], }, ], }); From 81f2cc8e555136bf3756f23766101586c816c402 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Fri, 20 Dec 2024 07:38:08 +0000 Subject: [PATCH 53/93] Remove e2e scripts --- eng/tools/eslint-plugin-tsv/package.json | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index 946631b2b9d5..31d6aa9830e8 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -13,22 +13,17 @@ "@types/node": "^18.19.31", "@vitest/coverage-v8": "^2.0.4", "eslint": "^9.17.0", - "memfs":"^4.15.0", + "memfs": "^4.15.0", "rimraf": "^5.0.10", "typescript": "~5.6.2", "vitest": "^2.0.4" }, "scripts": { "build": "tsc --build", + "cbt": "npm run clean && npm run build && npm run test:ci", "clean": "rimraf ./dist ./temp", "test": "vitest", - "test:ci": "vitest run --coverage --reporter=verbose", - "test:e2e": "npm run clean && npm run build && npm run test:e2e:all", - "test:e2e:all": "npm run test:e2e:contoso && npm run test:e2e:not-kebab-case-disabled && npm run test:e2e:not-kebab-case", - "test:e2e:contoso": "eslint --config dist/test/e2e/specification/eslint.config.js test/e2e/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml", - "test:e2e:not-kebab-case-disabled": "eslint --config dist/test/e2e/specification/eslint.config.js test/e2e/specification/Not-Kebab-Case-Disabled/Not.KebabCase/tspconfig.yaml", - "test:e2e:not-kebab-case": "eslint --config dist/test/e2e/specification/eslint.config.js test/e2e/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", - "test:all": "npm run clean && npm run build && npm run test:ci && npm run test:e2e:all" + "test:ci": "vitest run --coverage --reporter=verbose" }, "engines": { "node": ">= 18.0.0" From c7493c154ecd5a3868682a79d94e13719882c8da Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Sat, 21 Dec 2024 18:45:05 +0000 Subject: [PATCH 54/93] Remove problematic vitest config --- eng/tools/eslint-plugin-tsv/vitest.config.ts | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 eng/tools/eslint-plugin-tsv/vitest.config.ts diff --git a/eng/tools/eslint-plugin-tsv/vitest.config.ts b/eng/tools/eslint-plugin-tsv/vitest.config.ts deleted file mode 100644 index 785acc9b7335..000000000000 --- a/eng/tools/eslint-plugin-tsv/vitest.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - coverage: { - include: ["src"], - exclude: ["src/interfaces"], - }, - }, -}); From adbc241d21b48dba1c1ddf18a14ea3ab8a32f87a Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 24 Dec 2024 13:23:28 -0800 Subject: [PATCH 55/93] Revert "Remove problematic vitest config" This reverts commit c7493c154ecd5a3868682a79d94e13719882c8da. --- eng/tools/eslint-plugin-tsv/vitest.config.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 eng/tools/eslint-plugin-tsv/vitest.config.ts diff --git a/eng/tools/eslint-plugin-tsv/vitest.config.ts b/eng/tools/eslint-plugin-tsv/vitest.config.ts new file mode 100644 index 000000000000..785acc9b7335 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + coverage: { + include: ["src"], + exclude: ["src/interfaces"], + }, + }, +}); From 61fe81b8e4ce99eefa31e8bfd0ba91a24b2571a1 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 24 Dec 2024 13:39:20 -0800 Subject: [PATCH 56/93] Exclude vitest config from compilation - Prevents confusion caused if vite finds multiple config files (JS and TS) --- eng/tools/eslint-plugin-tsv/tsconfig.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/tsconfig.json b/eng/tools/eslint-plugin-tsv/tsconfig.json index ec6d6640928a..e97151ec5285 100644 --- a/eng/tools/eslint-plugin-tsv/tsconfig.json +++ b/eng/tools/eslint-plugin-tsv/tsconfig.json @@ -1,6 +1,11 @@ { "extends": "../tsconfig.json", "compilerOptions": { - "outDir": "./dist", - } + "outDir": "./dist" + }, + "exclude": [ + "node_modules", + "dist", + "vitest.config.ts" + ] } From 28a6aff2d3e231eaf05701396d74dc410d80a128 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Mon, 6 Jan 2025 22:30:22 +0000 Subject: [PATCH 57/93] [tsconfig.json] Replace excludes with includes --- eng/tools/eslint-plugin-tsv/tsconfig.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/tsconfig.json b/eng/tools/eslint-plugin-tsv/tsconfig.json index e97151ec5285..c16578a92bf1 100644 --- a/eng/tools/eslint-plugin-tsv/tsconfig.json +++ b/eng/tools/eslint-plugin-tsv/tsconfig.json @@ -3,9 +3,8 @@ "compilerOptions": { "outDir": "./dist" }, - "exclude": [ - "node_modules", - "dist", - "vitest.config.ts" + "include": [ + "src/**/*.ts", + "test/**/*.ts" ] } From 733db68762ce4908d723bc538e6afa0602eeb6f8 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 7 Jan 2025 03:34:45 +0000 Subject: [PATCH 58/93] Add comment --- eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts b/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts index ca1dc5df2397..fcc81c116138 100644 --- a/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts +++ b/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts @@ -1,6 +1,9 @@ import path from "path"; import { NamedRule } from "../interfaces/named-eslint.js"; +// Valid: /specification/kebab-case/Kebab.Case/tspconfig.yaml +// Invalid: /specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml + export const rule: NamedRule.RuleModule = { name: "kebab-case-org", meta: { From 3b7580edb7e0c3736eb0f6a872f7312def836dd0 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 7 Jan 2025 03:45:10 +0000 Subject: [PATCH 59/93] Add stub for new rule emit-autorest --- .../src/eslint-plugin-tsv.ts | 3 ++ .../src/rules/emit-autorest.ts | 47 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 eng/tools/eslint-plugin-tsv/src/rules/emit-autorest.ts diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts index f620058f6048..3e1867522c3f 100644 --- a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts +++ b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -1,5 +1,6 @@ import parser from "yaml-eslint-parser"; import { NamedESLint } from "./interfaces/named-eslint.js"; +import emitAutorest from "./rules/emit-autorest.js"; import kebabCaseOrg from "./rules/kebab-case-org.js"; const plugin: NamedESLint.Plugin = { @@ -7,6 +8,7 @@ const plugin: NamedESLint.Plugin = { name: "tsv", rules: { [kebabCaseOrg.name]: kebabCaseOrg, + [emitAutorest.name]: emitAutorest, }, }; @@ -17,6 +19,7 @@ plugin.configs.recommended = { files: ["*.yaml", "**/*.yaml"], rules: { [`${plugin.name}/${kebabCaseOrg.name}`]: "error", + [`${plugin.name}/${emitAutorest.name}`]: "error", }, languageOptions: { parser: parser, diff --git a/eng/tools/eslint-plugin-tsv/src/rules/emit-autorest.ts b/eng/tools/eslint-plugin-tsv/src/rules/emit-autorest.ts new file mode 100644 index 000000000000..216f3337bc90 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/rules/emit-autorest.ts @@ -0,0 +1,47 @@ +import { NamedRule } from "../interfaces/named-eslint.js"; + +export const rule: NamedRule.RuleModule = { + name: "emit-autorest", + meta: { + type: "problem", + docs: { + description: + "Requires emitter 'typespec-autorest' to be enabled by default, and requires emitted autorest to match content in repo", + }, + schema: [], + messages: { + disabled: "Path does not match format '.*/specification/{orgName}/': ''{{filename}}'", + autorestDiff: "Emitted autorest does not match content in repo", + }, + }, + create(context) { + return { + Program(node) { + // const filename = path.resolve(context.filename as string); + // const pathSegments = filename.split(path.sep); + // const specificationIndex = pathSegments.indexOf("specification"); + // const pathValid = specificationIndex >= 0 && specificationIndex < pathSegments.length - 1; + // if (!pathValid) { + // context.report({ + // node, + // messageId: "invalid", + // data: { filename: filename }, + // }); + // return; + // } + // const orgName = pathSegments[specificationIndex + 1]; + // const kebabCaseRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/; + // const orgNameKebabCase = orgName.match(kebabCaseRegex); + // if (!orgNameKebabCase) { + // context.report({ + // node, + // messageId: "kebab", + // data: { orgName: orgName }, + // }); + // } + }, + }; + }, +}; + +export default rule; From f5d864fb86f854770e46a8e04fa0a06dab70b129 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Tue, 7 Jan 2025 22:09:38 +0000 Subject: [PATCH 60/93] Validate tspconfig.yaml, ensure default emitter --- eng/tools/eslint-plugin-tsv/package.json | 1 + .../src/config/config-schema.ts | 117 ++++++++++++++++++ .../eslint-plugin-tsv/src/config/types.ts | 112 +++++++++++++++++ .../src/rules/emit-autorest.ts | 62 ++++++---- eng/tools/eslint-plugin-tsv/src/yaml/types.ts | 24 ++++ eng/tools/eslint-plugin-tsv/test/e2e.test.ts | 9 +- .../test/rules/emit-autorest.test.ts | 32 +++++ 7 files changed, 329 insertions(+), 28 deletions(-) create mode 100644 eng/tools/eslint-plugin-tsv/src/config/config-schema.ts create mode 100644 eng/tools/eslint-plugin-tsv/src/config/types.ts create mode 100644 eng/tools/eslint-plugin-tsv/src/yaml/types.ts create mode 100644 eng/tools/eslint-plugin-tsv/test/rules/emit-autorest.test.ts diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index 31d6aa9830e8..b124f469f81f 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -4,6 +4,7 @@ "type": "module", "main": "src/index.js", "dependencies": { + "ajv": "^8.17.1", "yaml-eslint-parser": "^1.2.3" }, "peerDependencies": { diff --git a/eng/tools/eslint-plugin-tsv/src/config/config-schema.ts b/eng/tools/eslint-plugin-tsv/src/config/config-schema.ts new file mode 100644 index 000000000000..e92b473ebf20 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/config/config-schema.ts @@ -0,0 +1,117 @@ +// Copied from https://github.com/microsoft/typespec/blob/main/packages/compiler/src/config/config-schema.ts + +import type { JSONSchemaType } from "ajv"; +import { EmitterOptions, TypeSpecRawConfig } from "./types.js"; + +export const emitterOptionsSchema: JSONSchemaType = { + type: "object", + additionalProperties: true, + required: [], + properties: { + "emitter-output-dir": { type: "string", nullable: true } as any, + }, +}; + +export const TypeSpecConfigJsonSchema: JSONSchemaType = { + type: "object", + additionalProperties: false, + properties: { + extends: { + type: "string", + nullable: true, + }, + "environment-variables": { + type: "object", + nullable: true, + required: [], + additionalProperties: { + type: "object", + properties: { + default: { type: "string" }, + }, + required: ["default"], + }, + }, + parameters: { + type: "object", + nullable: true, + required: [], + additionalProperties: { + type: "object", + properties: { + default: { type: "string" }, + }, + required: ["default"], + }, + }, + + "output-dir": { + type: "string", + nullable: true, + }, + "warn-as-error": { + type: "boolean", + nullable: true, + }, + trace: { + oneOf: [ + { type: "string" }, + { + type: "array", + items: { type: "string" }, + }, + ], + } as any, // Issue with AJV optional property typing https://github.com/ajv-validator/ajv/issues/1664 + imports: { + type: "array", + nullable: true, + items: { type: "string" }, + }, + emit: { + type: "array", + nullable: true, + items: { type: "string" }, + }, + options: { + type: "object", + nullable: true, + required: [], + additionalProperties: emitterOptionsSchema, + }, + emitters: { + type: "object", + nullable: true, + deprecated: true, + required: [], + additionalProperties: { + oneOf: [{ type: "boolean" }, emitterOptionsSchema], + }, + }, + + linter: { + type: "object", + nullable: true, + required: [], + additionalProperties: false, + properties: { + extends: { + type: "array", + nullable: true, + items: { type: "string" }, + }, + enable: { + type: "object", + required: [], + nullable: true, + additionalProperties: { type: "boolean" }, + }, + disable: { + type: "object", + required: [], + nullable: true, + additionalProperties: { type: "string" }, + }, + }, + } as any, // ajv type system doesn't like the string templates + }, +}; diff --git a/eng/tools/eslint-plugin-tsv/src/config/types.ts b/eng/tools/eslint-plugin-tsv/src/config/types.ts new file mode 100644 index 000000000000..575e3b6df553 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/config/types.ts @@ -0,0 +1,112 @@ +// Copied from https://github.com/microsoft/typespec/blob/main/packages/compiler/src/config/types.ts + +import type { Diagnostic, RuleRef } from "@typespec/compiler"; +import type { YamlScript } from "../yaml/types.js"; + +/** + * Represent the normalized user configuration. + */ +export interface TypeSpecConfig { + /** + * Project root. + */ + projectRoot: string; + + /** Yaml file used in this configuration. */ + file?: YamlScript; + + /** + * Path to the config file used to create this configuration. + */ + filename?: string; + + /** + * Diagnostics reported while loading the configuration + */ + diagnostics: Diagnostic[]; + + /** + * Path to another TypeSpec config to extend. + */ + extends?: string; + + /** + * Environment variables configuration + */ + environmentVariables?: Record; + + /** + * Parameters that can be used + */ + parameters?: Record; + + /** + * Treat warning as error. + */ + warnAsError?: boolean; + + /** + * Output directory + */ + outputDir: string; + + /** + * Trace options. + */ + trace?: string[]; + + /** + * Additional imports. + */ + imports?: string[]; + + /** + * Name of emitters or path to emitters that should be used. + */ + emit?: string[]; + + /** + * Name of emitters or path to emitters that should be used. + */ + options?: Record; + + linter?: LinterConfig; +} + +/** + * Represent the configuration that can be provided in a config file. + */ +export interface TypeSpecRawConfig { + extends?: string; + "environment-variables"?: Record; + parameters?: Record; + + "warn-as-error"?: boolean; + "output-dir"?: string; + trace?: string | string[]; + imports?: string[]; + + emit?: string[]; + options?: Record; + emitters?: Record; + + linter?: LinterConfig; +} + +export interface ConfigEnvironmentVariable { + default: string; +} + +export interface ConfigParameter { + default: string; +} + +export type EmitterOptions = Record & { + "emitter-output-dir"?: string; +}; + +export interface LinterConfig { + extends?: RuleRef[]; + enable?: Record; + disable?: Record; +} diff --git a/eng/tools/eslint-plugin-tsv/src/rules/emit-autorest.ts b/eng/tools/eslint-plugin-tsv/src/rules/emit-autorest.ts index 216f3337bc90..b64953d1ccbf 100644 --- a/eng/tools/eslint-plugin-tsv/src/rules/emit-autorest.ts +++ b/eng/tools/eslint-plugin-tsv/src/rules/emit-autorest.ts @@ -1,3 +1,8 @@ +import { Ajv } from "ajv"; +import { Rule } from "eslint"; +import { AST, getStaticYAMLValue } from "yaml-eslint-parser"; +import { TypeSpecConfigJsonSchema } from "../config/config-schema.js"; +import { TypeSpecConfig } from "../config/types.js"; import { NamedRule } from "../interfaces/named-eslint.js"; export const rule: NamedRule.RuleModule = { @@ -10,35 +15,42 @@ export const rule: NamedRule.RuleModule = { }, schema: [], messages: { - disabled: "Path does not match format '.*/specification/{orgName}/': ''{{filename}}'", - autorestDiff: "Emitted autorest does not match content in repo", + invalid: "tspconfig.yaml is invalid per the schema: {{errors}}", + missing: + 'tspconfig.yaml must include the following emitter by default:\n\nemit:\n - "@azure-tools/typespec-autorest"', + // disabled: "Path does not match format '.*/specification/{orgName}/': ''{{filename}}'", + // autorestDiff: "Emitted autorest does not match content in repo", }, }, create(context) { return { - Program(node) { - // const filename = path.resolve(context.filename as string); - // const pathSegments = filename.split(path.sep); - // const specificationIndex = pathSegments.indexOf("specification"); - // const pathValid = specificationIndex >= 0 && specificationIndex < pathSegments.length - 1; - // if (!pathValid) { - // context.report({ - // node, - // messageId: "invalid", - // data: { filename: filename }, - // }); - // return; - // } - // const orgName = pathSegments[specificationIndex + 1]; - // const kebabCaseRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/; - // const orgNameKebabCase = orgName.match(kebabCaseRegex); - // if (!orgNameKebabCase) { - // context.report({ - // node, - // messageId: "kebab", - // data: { orgName: orgName }, - // }); - // } + YAMLDocument(node: Rule.Node) { + const yamlDocument = node as unknown as AST.YAMLDocument; + + // If config yaml is empty, use empty object instead of "null" + const config = getStaticYAMLValue(yamlDocument) || {}; + + const ajv = new Ajv(); + const valid = ajv.validate(TypeSpecConfigJsonSchema, config); + + if (!valid) { + context.report({ + node, + messageId: "invalid", + data: { errors: ajv.errorsText(ajv.errors) }, + }); + return; + } + + const typedConfig = config as unknown as TypeSpecConfig; + if (!typedConfig.emit?.includes("@azure-tools/typespec-autorest")) { + // TODO: Move error message to "emit:" node + context.report({ + node, + messageId: "missing", + }); + return; + } }, }; }, diff --git a/eng/tools/eslint-plugin-tsv/src/yaml/types.ts b/eng/tools/eslint-plugin-tsv/src/yaml/types.ts new file mode 100644 index 000000000000..2c73c7c0da1e --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/yaml/types.ts @@ -0,0 +1,24 @@ +// Copied from https://github.com/microsoft/typespec/blob/main/packages/compiler/src/yaml/types.ts + +import { SourceFile } from "@typespec/compiler"; +import { Document } from "yaml"; + +export interface YamlScript { + readonly kind: "yaml-script"; + readonly file: SourceFile; + /** Value of the yaml script. */ + readonly value: unknown; + + /** @internal yaml library document. We do not expose this as the "yaml" library is not part of the contract. */ + readonly doc: Document.Parsed; +} + +/** + * Represent the location of a value in a yaml script. + */ +export interface YamlPathTarget { + kind: "path-target"; + script: YamlScript; + path: string[]; +} +export type YamlDiagnosticTargetType = "value" | "key"; diff --git a/eng/tools/eslint-plugin-tsv/test/e2e.test.ts b/eng/tools/eslint-plugin-tsv/test/e2e.test.ts index 5d28184c575a..7c8ed1376584 100644 --- a/eng/tools/eslint-plugin-tsv/test/e2e.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/e2e.test.ts @@ -28,9 +28,12 @@ describe("lint-text", () => { const filePath = "/specification/Not-Kebab-Case-Disabled/Not.KebabCase/tspconfig.yaml"; const eslint = createESLint(); - const results = await eslint.lintText("# eslint-disable tsv/kebab-case-org", { - filePath: filePath, - }); + const results = await eslint.lintText( + "# eslint-disable tsv/kebab-case-org, tsv/emit-autorest\n", + { + filePath: filePath, + }, + ); expect(results).toHaveLength(1); expect(results[0].filePath).toBe(filePath); diff --git a/eng/tools/eslint-plugin-tsv/test/rules/emit-autorest.test.ts b/eng/tools/eslint-plugin-tsv/test/rules/emit-autorest.test.ts new file mode 100644 index 000000000000..7f8938378634 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/test/rules/emit-autorest.test.ts @@ -0,0 +1,32 @@ +import { Rule, RuleTester } from "eslint"; +import { test } from "vitest"; +import parser from "yaml-eslint-parser"; + +import emitAutorest from "../../src/rules/emit-autorest.js"; + +test("RuleTester", () => { + const ruleTester = new RuleTester({ + languageOptions: { + parser: parser, + }, + }); + + ruleTester.run(emitAutorest.name, emitAutorest as Rule.RuleModule, { + valid: [ + { + code: 'emit:\n - "@azure-tools/typespec-autorest"', + }, + ], + invalid: [ + { + code: "", + errors: [{ messageId: "missing" }], + }, + { + code: "emit:\n - foo", + errors: [{ messageId: "missing" }], + }, + { code: "not: valid", errors: [{ messageId: "invalid" }] }, + ], + }); +}); From 6b5ebdbbd551b68cce47f0ac5c5b9b57efcbbf84 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Tue, 21 Jan 2025 09:25:19 +0800 Subject: [PATCH 61/93] add first test --- .../src/interfaces/rule-interfaces.ts | 19 +++++ ...-ts-mgmt-modular-generate-metadata-true.ts | 39 +++++++++ .../eslint-plugin-tsv/src/utils/constants.ts | 12 +++ eng/tools/eslint-plugin-tsv/src/utils/rule.ts | 32 +++++++ .../src/utils/tspconfig-validation-base.ts | 32 +++++++ .../tspconfig-options-validation.test.ts | 83 +++++++++++++++++++ 6 files changed, 217 insertions(+) create mode 100644 eng/tools/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts create mode 100644 eng/tools/eslint-plugin-tsv/src/rules/tspconfig-ts-mgmt-modular-generate-metadata-true.ts create mode 100644 eng/tools/eslint-plugin-tsv/src/utils/constants.ts create mode 100644 eng/tools/eslint-plugin-tsv/src/utils/rule.ts create mode 100644 eng/tools/eslint-plugin-tsv/src/utils/tspconfig-validation-base.ts create mode 100644 eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts diff --git a/eng/tools/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts b/eng/tools/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts new file mode 100644 index 000000000000..c32c7cec72ba --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts @@ -0,0 +1,19 @@ +import { Rule } from "eslint"; +import { TypeSpecConfig } from "../config/types.js"; + +export interface RuleDocuments { + description: string; + error: string; + action: string; + example: string; +} + +export interface RuleInfo { + name: string; + docs: RuleDocuments; + functions: { + messages: () => { [messageId: string]: string } | undefined; + condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => boolean; + validation: (tspconfig: TypeSpecConfig, context: Rule.RuleContext, node: Rule.Node) => void; + }; +} diff --git a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-ts-mgmt-modular-generate-metadata-true.ts b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-ts-mgmt-modular-generate-metadata-true.ts new file mode 100644 index 000000000000..ca03579a8e56 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-ts-mgmt-modular-generate-metadata-true.ts @@ -0,0 +1,39 @@ +import { RuleDocuments, RuleInfo } from "../interfaces/rule-interfaces.js"; +import { defaultMessageId, emitters } from "../utils/constants.js"; +import { isManagementForTsEmitter, makeRuleMessages } from "../utils/rule.js"; +import { createRule } from "../utils/tspconfig-validation-base.js"; + +const docs: RuleDocuments = { + description: + "Validate whether 'generateMetadata' is set to true in tspconfig.yaml when generating modular clients", + error: "'generateMetadata' is NOT set to true in tspconfig.yaml when generating modular clients", + action: `Set 'options.${emitters.ts}.generateMetadata' to true in tspconfig.yaml when generating modular clients`, + example: `... +options: + "@azure-tools/typespec-ts": + generateMetadata: true # <--- + generateSample: true + generateTest: true + experimentalExtensibleEnums: true + enableOperationGroup: true + hierarchyClient: false + package-dir: "pkg" + packageDetails: + name: "@azure/pkg" + flavor: azure +`, +}; +const ruleInfo: RuleInfo = { + name: "tspconfig-ts-mgmt-modular-generate-metadata-true", + docs, + functions: { + messages: () => makeRuleMessages(defaultMessageId, docs), + condition: (tspconfig, context) => isManagementForTsEmitter(tspconfig, context), + validation: (tspconfig, context, node) => { + const generateMetadata = tspconfig.options?.[emitters.ts].generateMetadata; + if (generateMetadata !== true) context.report({ node, messageId: defaultMessageId }); + }, + }, +}; + +export default createRule(ruleInfo); diff --git a/eng/tools/eslint-plugin-tsv/src/utils/constants.ts b/eng/tools/eslint-plugin-tsv/src/utils/constants.ts new file mode 100644 index 000000000000..2ca356ba4f48 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/utils/constants.ts @@ -0,0 +1,12 @@ +export const emitters = { + ts: "@azure-tools/typespec-ts", + java: "@azure-tools/typespec-java", + csharp: "@azure-tools/typespec-csharp", + python: "@azure-tools/typespec-python", + go: "@azure-tools/typespec-go", + autorest: "@azure-tools/typespec-autorest", +}; + +export const defaultMessageId = "problem"; + +export const defaultRuleType = "problem"; diff --git a/eng/tools/eslint-plugin-tsv/src/utils/rule.ts b/eng/tools/eslint-plugin-tsv/src/utils/rule.ts new file mode 100644 index 000000000000..d2768dd96b28 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/utils/rule.ts @@ -0,0 +1,32 @@ +import { Rule } from "eslint"; +import { TypeSpecConfig } from "../config/types.js"; +import { RuleDocuments } from "../interfaces/rule-interfaces.js"; +import { emitters } from "./constants.js"; + +export function makeRuleMessages(messageId: string, docs: RuleDocuments) { + return { + [messageId]: `${docs.error}.\n${docs.action}.\n${docs.example}`, + }; +} + +export function isManagementForTsEmitter(tspconfig: TypeSpecConfig, context: Rule.RuleContext) { + const flavor = tspconfig.options?.[emitters.ts]?.flavor as string; + const isModularLibrary = tspconfig.options?.[emitters.ts]?.isModularLibrary as + | boolean + | undefined; + const filename = context.filename; + return flavor === "azure" && filename.includes(".Management") && isModularLibrary == undefined; +} + +export function generateEmitterOptions( + emitter: string, + ...pairs: { key: string; value: string | boolean | {} }[] +) { + let content = `options: + "${emitter}":`; + for (const pair of pairs) { + content += ` + ${pair.key}: ${pair.value}`; + } + return content; +} diff --git a/eng/tools/eslint-plugin-tsv/src/utils/tspconfig-validation-base.ts b/eng/tools/eslint-plugin-tsv/src/utils/tspconfig-validation-base.ts new file mode 100644 index 000000000000..a36503084c85 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/utils/tspconfig-validation-base.ts @@ -0,0 +1,32 @@ +import { Rule } from "eslint"; +import { AST, getStaticYAMLValue } from "yaml-eslint-parser"; +import { TypeSpecConfig } from "../config/types.js"; +import { NamedRule } from "../interfaces/named-eslint.js"; +import { RuleInfo } from "../interfaces/rule-interfaces.js"; +import { defaultRuleType } from "./constants.js"; + +export function createRule(ruleContext: RuleInfo) { + const rule: NamedRule.RuleModule = { + name: ruleContext.name, + meta: { + type: defaultRuleType, + docs: { + description: ruleContext.docs.description, + }, + schema: [], + messages: ruleContext.functions.messages(), + }, + create(context) { + return { + YAMLDocument(node: Rule.Node) { + const yamlDocument = node as unknown as AST.YAMLDocument; + const config = getStaticYAMLValue(yamlDocument) || {}; + const typedConfig = config as unknown as TypeSpecConfig; + if (!ruleContext.functions.condition(typedConfig, context)) return; + ruleContext.functions.validation(typedConfig, context, node); + }, + }; + }, + }; + return rule; +} diff --git a/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts b/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts new file mode 100644 index 000000000000..e1f6adbdc79d --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts @@ -0,0 +1,83 @@ +import { Rule, RuleTester } from "eslint"; +import { describe, it } from "vitest"; +import parser from "yaml-eslint-parser"; +import { defaultMessageId, emitters } from "../../src/utils/constants.js"; +import { generateEmitterOptions } from "../../src/utils/rule.js"; + +interface Case { + description: string; + rulePath: string; + fileName?: string; + yamlContent: string; + shouldReportError: boolean; +} + +const managementTspconfigPath = "contosowidgetmanager/Contoso.Management/tspconfig.yaml"; +const tspconfigTsMgmtModularGenerateMetadataTrueRule = + "../../src/rules/tspconfig-ts-mgmt-modular-generate-metadata-true.js"; + +const managementGenerateMetadataTestCases: Case[] = [ + { + description: "valid: generateMetadata is true", + rulePath: tspconfigTsMgmtModularGenerateMetadataTrueRule, + fileName: managementTspconfigPath, + yamlContent: generateEmitterOptions( + emitters.ts, + { key: "generateMetadata", value: true }, + { key: "flavor", value: "azure" }, + ), + shouldReportError: false, + }, + { + description: "invalid: generateMetadata is false", + rulePath: tspconfigTsMgmtModularGenerateMetadataTrueRule, + fileName: managementTspconfigPath, + yamlContent: generateEmitterOptions( + emitters.ts, + { key: "generateMetadata", value: false }, + { key: "flavor", value: "azure" }, + ), + shouldReportError: true, + }, + { + description: "invalid: generateMetadata is undefined", + rulePath: tspconfigTsMgmtModularGenerateMetadataTrueRule, + fileName: managementTspconfigPath, + yamlContent: generateEmitterOptions(emitters.ts, { key: "flavor", value: "azure" }), + shouldReportError: true, + }, +]; + +describe("Tspconfig emitter options validation", () => { + it.each([...managementGenerateMetadataTestCases])("$description", async (c: Case) => { + const ruleTester = new RuleTester({ + languageOptions: { + parser: parser, + }, + }); + + const ruleModule = await import(c.rulePath); + const rule = ruleModule.default; + const tests = c.shouldReportError + ? { + valid: [], + invalid: [ + { + filename: c.fileName, + code: c.yamlContent, + errors: [{ messageId: defaultMessageId }], + }, + ], + } + : { + valid: [ + { + filename: c.fileName, + code: c.yamlContent, + }, + ], + invalid: [], + }; + ruleTester.run(rule.name, rule as Rule.RuleModule, tests); + }); +}); From 4c419809bf65d84a07b9f06b021da747a6bb2490 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Tue, 21 Jan 2025 13:58:36 +0800 Subject: [PATCH 62/93] simplify for adding new rules --- .../src/interfaces/rule-interfaces.ts | 4 +- ...-ts-mgmt-modular-generate-metadata-true.ts | 39 ------ .../src/rules/tspconfig-validation-rules.ts | 21 +++ eng/tools/eslint-plugin-tsv/src/utils/rule.ts | 77 ++++++++++- .../src/utils/tspconfig-validation-base.ts | 32 ----- .../tspconfig-options-validation.test.ts | 125 +++++++++++++----- 6 files changed, 187 insertions(+), 111 deletions(-) delete mode 100644 eng/tools/eslint-plugin-tsv/src/rules/tspconfig-ts-mgmt-modular-generate-metadata-true.ts create mode 100644 eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts delete mode 100644 eng/tools/eslint-plugin-tsv/src/utils/tspconfig-validation-base.ts diff --git a/eng/tools/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts b/eng/tools/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts index c32c7cec72ba..4ea789d7ba86 100644 --- a/eng/tools/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts +++ b/eng/tools/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts @@ -1,7 +1,7 @@ import { Rule } from "eslint"; import { TypeSpecConfig } from "../config/types.js"; -export interface RuleDocuments { +export interface RuleDocument { description: string; error: string; action: string; @@ -10,7 +10,7 @@ export interface RuleDocuments { export interface RuleInfo { name: string; - docs: RuleDocuments; + documentation: RuleDocument; functions: { messages: () => { [messageId: string]: string } | undefined; condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => boolean; diff --git a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-ts-mgmt-modular-generate-metadata-true.ts b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-ts-mgmt-modular-generate-metadata-true.ts deleted file mode 100644 index ca03579a8e56..000000000000 --- a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-ts-mgmt-modular-generate-metadata-true.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { RuleDocuments, RuleInfo } from "../interfaces/rule-interfaces.js"; -import { defaultMessageId, emitters } from "../utils/constants.js"; -import { isManagementForTsEmitter, makeRuleMessages } from "../utils/rule.js"; -import { createRule } from "../utils/tspconfig-validation-base.js"; - -const docs: RuleDocuments = { - description: - "Validate whether 'generateMetadata' is set to true in tspconfig.yaml when generating modular clients", - error: "'generateMetadata' is NOT set to true in tspconfig.yaml when generating modular clients", - action: `Set 'options.${emitters.ts}.generateMetadata' to true in tspconfig.yaml when generating modular clients`, - example: `... -options: - "@azure-tools/typespec-ts": - generateMetadata: true # <--- - generateSample: true - generateTest: true - experimentalExtensibleEnums: true - enableOperationGroup: true - hierarchyClient: false - package-dir: "pkg" - packageDetails: - name: "@azure/pkg" - flavor: azure -`, -}; -const ruleInfo: RuleInfo = { - name: "tspconfig-ts-mgmt-modular-generate-metadata-true", - docs, - functions: { - messages: () => makeRuleMessages(defaultMessageId, docs), - condition: (tspconfig, context) => isManagementForTsEmitter(tspconfig, context), - validation: (tspconfig, context, node) => { - const generateMetadata = tspconfig.options?.[emitters.ts].generateMetadata; - if (generateMetadata !== true) context.report({ node, messageId: defaultMessageId }); - }, - }, -}; - -export default createRule(ruleInfo); diff --git a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts new file mode 100644 index 000000000000..18d463d69fb4 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts @@ -0,0 +1,21 @@ +import { emitters } from "../utils/constants.js"; +import { createManagementClientRule } from "../utils/rule.js"; + +const args: [string, string, string, string | boolean][] = [ + ["tspconfig-ts-mgmt-modular-generate-metadata-true", emitters.ts, "generateMetadata", true], + ["tspconfig-ts-mgmt-modular-hierarchy-client-false", emitters.ts, "hierarchyClient", false], + [ + "tspconfig-ts-mgmt-modular-experimental-extensible-enums-true", + emitters.ts, + "experimentalExtensibleEnums", + true, + ], + [ + "tspconfig-ts-mgmt-modular-enable-operation-group-true", + emitters.ts, + "enableOperationGroup", + true, + ], +]; + +export default args.map((a) => createManagementClientRule(...a)); diff --git a/eng/tools/eslint-plugin-tsv/src/utils/rule.ts b/eng/tools/eslint-plugin-tsv/src/utils/rule.ts index d2768dd96b28..304f63fb7c21 100644 --- a/eng/tools/eslint-plugin-tsv/src/utils/rule.ts +++ b/eng/tools/eslint-plugin-tsv/src/utils/rule.ts @@ -1,9 +1,56 @@ import { Rule } from "eslint"; import { TypeSpecConfig } from "../config/types.js"; -import { RuleDocuments } from "../interfaces/rule-interfaces.js"; -import { emitters } from "./constants.js"; +import { RuleDocument, RuleInfo } from "../interfaces/rule-interfaces.js"; +import { defaultMessageId, defaultRuleType, emitters } from "./constants.js"; +import { NamedRule } from "../interfaces/named-eslint.js"; +import { AST, getStaticYAMLValue } from "yaml-eslint-parser"; -export function makeRuleMessages(messageId: string, docs: RuleDocuments) { +function createManagementRuleDocument( + emitterName: string, + optionName: string, + expectedOptionValue: string | boolean, +): RuleDocument { + const document: RuleDocument = { + description: `Validate whether '${optionName}' is set to true in tspconfig.yaml when generating modular clients`, + error: `'${optionName}' is NOT set to true in tspconfig.yaml when generating modular clients`, + action: `Set 'options.${emitters.ts}.${optionName}' to true in tspconfig.yaml when generating modular clients`, + example: `... + options: + "${emitterName}": + ${optionName}: ${expectedOptionValue} # <--- SET HERE + ... + `, + }; + return document; +} + +export function createRule(ruleContext: RuleInfo): NamedRule.RuleModule { + const rule: NamedRule.RuleModule = { + name: ruleContext.name, + meta: { + type: defaultRuleType, + docs: { + description: ruleContext.documentation.description, + }, + schema: [], + messages: ruleContext.functions.messages(), + }, + create(context) { + return { + YAMLDocument(node: Rule.Node) { + const yamlDocument = node as unknown as AST.YAMLDocument; + const config = getStaticYAMLValue(yamlDocument) || {}; + const typedConfig = config as unknown as TypeSpecConfig; + if (!ruleContext.functions.condition(typedConfig, context)) return; + ruleContext.functions.validation(typedConfig, context, node); + }, + }; + }, + }; + return rule; +} + +export function createRuleMessages(messageId: string, docs: RuleDocument) { return { [messageId]: `${docs.error}.\n${docs.action}.\n${docs.example}`, }; @@ -30,3 +77,27 @@ export function generateEmitterOptions( } return content; } + +export function createManagementClientRule( + ruleName: string, + emitterName: string, + optionName: string, + expectedOptionValue: string | boolean, +): NamedRule.RuleModule { + const documentation = createManagementRuleDocument(emitterName, optionName, expectedOptionValue); + + const ruleInfo: RuleInfo = { + name: ruleName, + documentation, + functions: { + messages: () => createRuleMessages(defaultMessageId, documentation), + condition: (tspconfig, context) => isManagementForTsEmitter(tspconfig, context), + validation: (tspconfig, context, node) => { + const option = tspconfig.options?.[emitters.ts][optionName]; + if (option !== expectedOptionValue) context.report({ node, messageId: defaultMessageId }); + }, + }, + }; + + return createRule(ruleInfo); +} diff --git a/eng/tools/eslint-plugin-tsv/src/utils/tspconfig-validation-base.ts b/eng/tools/eslint-plugin-tsv/src/utils/tspconfig-validation-base.ts deleted file mode 100644 index a36503084c85..000000000000 --- a/eng/tools/eslint-plugin-tsv/src/utils/tspconfig-validation-base.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Rule } from "eslint"; -import { AST, getStaticYAMLValue } from "yaml-eslint-parser"; -import { TypeSpecConfig } from "../config/types.js"; -import { NamedRule } from "../interfaces/named-eslint.js"; -import { RuleInfo } from "../interfaces/rule-interfaces.js"; -import { defaultRuleType } from "./constants.js"; - -export function createRule(ruleContext: RuleInfo) { - const rule: NamedRule.RuleModule = { - name: ruleContext.name, - meta: { - type: defaultRuleType, - docs: { - description: ruleContext.docs.description, - }, - schema: [], - messages: ruleContext.functions.messages(), - }, - create(context) { - return { - YAMLDocument(node: Rule.Node) { - const yamlDocument = node as unknown as AST.YAMLDocument; - const config = getStaticYAMLValue(yamlDocument) || {}; - const typedConfig = config as unknown as TypeSpecConfig; - if (!ruleContext.functions.condition(typedConfig, context)) return; - ruleContext.functions.validation(typedConfig, context, node); - }, - }; - }, - }; - return rule; -} diff --git a/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts b/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts index e1f6adbdc79d..bd283f5da2b3 100644 --- a/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts @@ -3,53 +3,108 @@ import { describe, it } from "vitest"; import parser from "yaml-eslint-parser"; import { defaultMessageId, emitters } from "../../src/utils/constants.js"; import { generateEmitterOptions } from "../../src/utils/rule.js"; +import { NamedRule } from "../../src/interfaces/named-eslint.js"; interface Case { description: string; rulePath: string; + ruleName: string; fileName?: string; yamlContent: string; shouldReportError: boolean; } const managementTspconfigPath = "contosowidgetmanager/Contoso.Management/tspconfig.yaml"; -const tspconfigTsMgmtModularGenerateMetadataTrueRule = - "../../src/rules/tspconfig-ts-mgmt-modular-generate-metadata-true.js"; +const rulePath = "../../src/rules/tspconfig-validation-rules.js"; -const managementGenerateMetadataTestCases: Case[] = [ - { - description: "valid: generateMetadata is true", - rulePath: tspconfigTsMgmtModularGenerateMetadataTrueRule, - fileName: managementTspconfigPath, - yamlContent: generateEmitterOptions( - emitters.ts, - { key: "generateMetadata", value: true }, - { key: "flavor", value: "azure" }, - ), - shouldReportError: false, - }, - { - description: "invalid: generateMetadata is false", - rulePath: tspconfigTsMgmtModularGenerateMetadataTrueRule, - fileName: managementTspconfigPath, - yamlContent: generateEmitterOptions( - emitters.ts, - { key: "generateMetadata", value: false }, - { key: "flavor", value: "azure" }, - ), - shouldReportError: true, - }, - { - description: "invalid: generateMetadata is undefined", - rulePath: tspconfigTsMgmtModularGenerateMetadataTrueRule, - fileName: managementTspconfigPath, - yamlContent: generateEmitterOptions(emitters.ts, { key: "flavor", value: "azure" }), - shouldReportError: true, - }, -]; +const managementGenerateMetadataTestCases = generateManagementClientBooleanTestCases( + emitters.ts, + rulePath, + "tspconfig-ts-mgmt-modular-generate-metadata-true", + managementTspconfigPath, + "generateMetadata", + true, +); + +const managementHierarchyClientTestCases = generateManagementClientBooleanTestCases( + emitters.ts, + rulePath, + "tspconfig-ts-mgmt-modular-hierarchy-client-false", + managementTspconfigPath, + "hierarchyClient", + false, +); + +const managementExperimentalExtensibleEnumsTestCases = generateManagementClientBooleanTestCases( + emitters.ts, + rulePath, + "tspconfig-ts-mgmt-modular-experimental-extensible-enums-true", + managementTspconfigPath, + "experimentalExtensibleEnums", + true, +); + +const managementEnableOperationGroupTestCases = generateManagementClientBooleanTestCases( + emitters.ts, + rulePath, + "tspconfig-ts-mgmt-modular-enable-operation-group-true", + managementTspconfigPath, + "enableOperationGroup", + true, +); + +function generateManagementClientBooleanTestCases( + emitterName: string, + rulePath: string, + ruleName: string, + fileName: string, + optionName: string, + expectedOptionValue: boolean, +): Case[] { + const managementGenerateMetadataTestCases: Case[] = [ + { + description: `valid: ${optionName} is ${expectedOptionValue}`, + rulePath, + ruleName, + fileName, + yamlContent: generateEmitterOptions( + emitterName, + { key: optionName, value: expectedOptionValue }, + { key: "flavor", value: "azure" }, + ), + shouldReportError: false, + }, + { + description: `invalid: ${optionName} is ${!expectedOptionValue}`, + rulePath, + ruleName, + fileName, + yamlContent: generateEmitterOptions( + emitterName, + { key: optionName, value: !expectedOptionValue }, + { key: "flavor", value: "azure" }, + ), + shouldReportError: true, + }, + { + description: `invalid: ${optionName} is undefined`, + rulePath, + ruleName, + fileName, + yamlContent: generateEmitterOptions(emitterName, { key: "flavor", value: "azure" }), + shouldReportError: true, + }, + ]; + return managementGenerateMetadataTestCases; +} describe("Tspconfig emitter options validation", () => { - it.each([...managementGenerateMetadataTestCases])("$description", async (c: Case) => { + it.each([ + ...managementGenerateMetadataTestCases, + ...managementHierarchyClientTestCases, + ...managementExperimentalExtensibleEnumsTestCases, + ...managementEnableOperationGroupTestCases, + ])("$ruleName - $description", async (c: Case) => { const ruleTester = new RuleTester({ languageOptions: { parser: parser, @@ -57,7 +112,7 @@ describe("Tspconfig emitter options validation", () => { }); const ruleModule = await import(c.rulePath); - const rule = ruleModule.default; + const rule = ruleModule.default.find((r: NamedRule.RuleModule) => r.name === c.ruleName); const tests = c.shouldReportError ? { valid: [], From b1ae1a3b40125d14c0150f218faa90abff0fe164 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Tue, 21 Jan 2025 15:18:02 +0800 Subject: [PATCH 63/93] all js rules added --- .../src/rules/tspconfig-validation-rules.ts | 28 +++++++- eng/tools/eslint-plugin-tsv/src/utils/rule.ts | 65 +++++++++++++------ .../tspconfig-options-validation.test.ts | 55 ++++++++++++---- 3 files changed, 112 insertions(+), 36 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts index 18d463d69fb4..d8214e5155a7 100644 --- a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts +++ b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts @@ -1,20 +1,42 @@ import { emitters } from "../utils/constants.js"; import { createManagementClientRule } from "../utils/rule.js"; -const args: [string, string, string, string | boolean][] = [ - ["tspconfig-ts-mgmt-modular-generate-metadata-true", emitters.ts, "generateMetadata", true], - ["tspconfig-ts-mgmt-modular-hierarchy-client-false", emitters.ts, "hierarchyClient", false], +const args: [string, string, string, string | boolean | RegExp, string | boolean][] = [ + ["tspconfig-ts-mgmt-modular-generate-metadata-true", emitters.ts, "generateMetadata", true, true], + [ + "tspconfig-ts-mgmt-modular-hierarchy-client-false", + emitters.ts, + "hierarchyClient", + false, + false, + ], [ "tspconfig-ts-mgmt-modular-experimental-extensible-enums-true", emitters.ts, "experimentalExtensibleEnums", true, + true, ], [ "tspconfig-ts-mgmt-modular-enable-operation-group-true", emitters.ts, "enableOperationGroup", true, + true, + ], + [ + "tspconfig-ts-mgmt-modular-package-dir-match-pattern", + emitters.ts, + "package-dir", + /^arm(?:-[a-z]+)+$/, + "arm-aaa-bbb", + ], + [ + "tspconfig-ts-mgmt-modular-package-name-match-pattern", + emitters.ts, + "packageDetails.name", + /^\@azure\/arm(?:-[a-z]+)+$/, + "@azure/arm-aaa-bbb", ], ]; diff --git a/eng/tools/eslint-plugin-tsv/src/utils/rule.ts b/eng/tools/eslint-plugin-tsv/src/utils/rule.ts index 304f63fb7c21..8de790b788a1 100644 --- a/eng/tools/eslint-plugin-tsv/src/utils/rule.ts +++ b/eng/tools/eslint-plugin-tsv/src/utils/rule.ts @@ -4,22 +4,20 @@ import { RuleDocument, RuleInfo } from "../interfaces/rule-interfaces.js"; import { defaultMessageId, defaultRuleType, emitters } from "./constants.js"; import { NamedRule } from "../interfaces/named-eslint.js"; import { AST, getStaticYAMLValue } from "yaml-eslint-parser"; +import { stringify } from "yaml"; function createManagementRuleDocument( emitterName: string, optionName: string, - expectedOptionValue: string | boolean, + expectedOptionValue: string | boolean | RegExp, + exampleValue: string | boolean, ): RuleDocument { const document: RuleDocument = { - description: `Validate whether '${optionName}' is set to true in tspconfig.yaml when generating modular clients`, - error: `'${optionName}' is NOT set to true in tspconfig.yaml when generating modular clients`, - action: `Set 'options.${emitters.ts}.${optionName}' to true in tspconfig.yaml when generating modular clients`, - example: `... - options: - "${emitterName}": - ${optionName}: ${expectedOptionValue} # <--- SET HERE - ... - `, + // TODO: improve description with natual language + description: `Validate whether 'options.${emitters.ts}.${optionName}' is set to '${expectedOptionValue}' in tspconfig.yaml when generating modular clients`, + error: `'options.${emitters.ts}.${optionName}' is NOT set to '${expectedOptionValue}' in tspconfig.yaml when generating modular clients`, + action: `Set 'options.${emitters.ts}.${optionName}' to '${expectedOptionValue}' in tspconfig.yaml when generating modular clients`, + example: createEmitterOptions(emitterName, { key: optionName, value: exampleValue }), }; return document; } @@ -65,16 +63,26 @@ export function isManagementForTsEmitter(tspconfig: TypeSpecConfig, context: Rul return flavor === "azure" && filename.includes(".Management") && isModularLibrary == undefined; } -export function generateEmitterOptions( +export function createEmitterOptions( emitter: string, ...pairs: { key: string; value: string | boolean | {} }[] ) { - let content = `options: - "${emitter}":`; + const obj = { options: { [emitter]: {} } }; for (const pair of pairs) { - content += ` - ${pair.key}: ${pair.value}`; + const segments = pair.key.split("."); + let cur: { [id: string]: any } = obj.options[emitter]; + for (const [i, segment] of segments.entries()) { + if (i === segments.length - 1) { + cur[segment] = pair.value; + break; + } + if (!(segment in cur)) { + cur[segment] = {}; + } + cur = cur[segment]; + } } + const content = stringify(obj); return content; } @@ -82,9 +90,15 @@ export function createManagementClientRule( ruleName: string, emitterName: string, optionName: string, - expectedOptionValue: string | boolean, + expectedOptionValue: string | boolean | RegExp, + exampleValue: string | boolean, ): NamedRule.RuleModule { - const documentation = createManagementRuleDocument(emitterName, optionName, expectedOptionValue); + const documentation = createManagementRuleDocument( + emitterName, + optionName, + expectedOptionValue, + exampleValue, + ); const ruleInfo: RuleInfo = { name: ruleName, @@ -93,8 +107,21 @@ export function createManagementClientRule( messages: () => createRuleMessages(defaultMessageId, documentation), condition: (tspconfig, context) => isManagementForTsEmitter(tspconfig, context), validation: (tspconfig, context, node) => { - const option = tspconfig.options?.[emitters.ts][optionName]; - if (option !== expectedOptionValue) context.report({ node, messageId: defaultMessageId }); + let option: any = tspconfig.options?.[emitters.ts]; + for (const segment of optionName.split(".")) { + if (segment in option) option = option[segment]; + } + switch (typeof expectedOptionValue) { + case "boolean": + case "string": + if (option !== expectedOptionValue) + context.report({ node, messageId: defaultMessageId }); + break; + case "object": + if (typeof option !== "string" || !expectedOptionValue.test(option)) + context.report({ node, messageId: defaultMessageId }); + break; + } }, }, }; diff --git a/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts b/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts index bd283f5da2b3..2b1c1f1ecd5c 100644 --- a/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts @@ -2,7 +2,7 @@ import { Rule, RuleTester } from "eslint"; import { describe, it } from "vitest"; import parser from "yaml-eslint-parser"; import { defaultMessageId, emitters } from "../../src/utils/constants.js"; -import { generateEmitterOptions } from "../../src/utils/rule.js"; +import { createEmitterOptions } from "../../src/utils/rule.js"; import { NamedRule } from "../../src/interfaces/named-eslint.js"; interface Case { @@ -17,71 +17,96 @@ interface Case { const managementTspconfigPath = "contosowidgetmanager/Contoso.Management/tspconfig.yaml"; const rulePath = "../../src/rules/tspconfig-validation-rules.js"; -const managementGenerateMetadataTestCases = generateManagementClientBooleanTestCases( +const managementGenerateMetadataTestCases = generateManagementClientTestCases( emitters.ts, rulePath, "tspconfig-ts-mgmt-modular-generate-metadata-true", managementTspconfigPath, "generateMetadata", true, + false, ); -const managementHierarchyClientTestCases = generateManagementClientBooleanTestCases( +const managementHierarchyClientTestCases = generateManagementClientTestCases( emitters.ts, rulePath, "tspconfig-ts-mgmt-modular-hierarchy-client-false", managementTspconfigPath, "hierarchyClient", false, + true, ); -const managementExperimentalExtensibleEnumsTestCases = generateManagementClientBooleanTestCases( +const managementExperimentalExtensibleEnumsTestCases = generateManagementClientTestCases( emitters.ts, rulePath, "tspconfig-ts-mgmt-modular-experimental-extensible-enums-true", managementTspconfigPath, "experimentalExtensibleEnums", true, + false, ); -const managementEnableOperationGroupTestCases = generateManagementClientBooleanTestCases( +const managementEnableOperationGroupTestCases = generateManagementClientTestCases( emitters.ts, rulePath, "tspconfig-ts-mgmt-modular-enable-operation-group-true", managementTspconfigPath, "enableOperationGroup", true, + false, +); + +const managementPackageDirTestCases = generateManagementClientTestCases( + emitters.ts, + rulePath, + "tspconfig-ts-mgmt-modular-package-dir-match-pattern", + managementTspconfigPath, + "package-dir", + "arm-aaa-bbb", + "aaa-bbb", +); + +const managementPackageNameTestCases = generateManagementClientTestCases( + emitters.ts, + rulePath, + "tspconfig-ts-mgmt-modular-package-name-match-pattern", + managementTspconfigPath, + "packageDetails.name", + "@azure/arm-aaa-bbb", + "@azure/aaa-bbb", ); -function generateManagementClientBooleanTestCases( +function generateManagementClientTestCases( emitterName: string, rulePath: string, ruleName: string, fileName: string, optionName: string, - expectedOptionValue: boolean, + validOptionValue: boolean | string, + invalidOptionValue: boolean | string, ): Case[] { const managementGenerateMetadataTestCases: Case[] = [ { - description: `valid: ${optionName} is ${expectedOptionValue}`, + description: `valid: ${optionName} is ${validOptionValue}`, rulePath, ruleName, fileName, - yamlContent: generateEmitterOptions( + yamlContent: createEmitterOptions( emitterName, - { key: optionName, value: expectedOptionValue }, + { key: optionName, value: validOptionValue }, { key: "flavor", value: "azure" }, ), shouldReportError: false, }, { - description: `invalid: ${optionName} is ${!expectedOptionValue}`, + description: `invalid: ${optionName} is ${invalidOptionValue}`, rulePath, ruleName, fileName, - yamlContent: generateEmitterOptions( + yamlContent: createEmitterOptions( emitterName, - { key: optionName, value: !expectedOptionValue }, + { key: optionName, value: invalidOptionValue }, { key: "flavor", value: "azure" }, ), shouldReportError: true, @@ -91,7 +116,7 @@ function generateManagementClientBooleanTestCases( rulePath, ruleName, fileName, - yamlContent: generateEmitterOptions(emitterName, { key: "flavor", value: "azure" }), + yamlContent: createEmitterOptions(emitterName, { key: "flavor", value: "azure" }), shouldReportError: true, }, ]; @@ -104,6 +129,8 @@ describe("Tspconfig emitter options validation", () => { ...managementHierarchyClientTestCases, ...managementExperimentalExtensibleEnumsTestCases, ...managementEnableOperationGroupTestCases, + ...managementPackageDirTestCases, + ...managementPackageNameTestCases, ])("$ruleName - $description", async (c: Case) => { const ruleTester = new RuleTester({ languageOptions: { From 40c65f02dcbfb151af7288941fb9126c2edd4427 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Tue, 21 Jan 2025 18:44:34 +0800 Subject: [PATCH 64/93] added go rules --- .../src/rules/tspconfig-validation-rules.ts | 49 ++-- .../src/utils/config-interpolation.ts | 221 ++++++++++++++++++ eng/tools/eslint-plugin-tsv/src/utils/rule.ts | 58 +++-- .../tspconfig-options-validation.test.ts | 114 ++++++++- 4 files changed, 401 insertions(+), 41 deletions(-) create mode 100644 eng/tools/eslint-plugin-tsv/src/utils/config-interpolation.ts diff --git a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts index d8214e5155a7..adf88d940aed 100644 --- a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts +++ b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts @@ -1,43 +1,64 @@ -import { emitters } from "../utils/constants.js"; import { createManagementClientRule } from "../utils/rule.js"; -const args: [string, string, string, string | boolean | RegExp, string | boolean][] = [ - ["tspconfig-ts-mgmt-modular-generate-metadata-true", emitters.ts, "generateMetadata", true, true], - [ - "tspconfig-ts-mgmt-modular-hierarchy-client-false", - emitters.ts, - "hierarchyClient", - false, - false, - ], +const args: [string, string, string | boolean | RegExp, string | boolean, string | undefined][] = [ + // ts + ["tspconfig-ts-mgmt-modular-generate-metadata-true", "generateMetadata", true, true, undefined], + ["tspconfig-ts-mgmt-modular-hierarchy-client-false", "hierarchyClient", false, false, undefined], [ "tspconfig-ts-mgmt-modular-experimental-extensible-enums-true", - emitters.ts, "experimentalExtensibleEnums", true, true, + undefined, ], [ "tspconfig-ts-mgmt-modular-enable-operation-group-true", - emitters.ts, "enableOperationGroup", true, true, + undefined, ], [ "tspconfig-ts-mgmt-modular-package-dir-match-pattern", - emitters.ts, "package-dir", /^arm(?:-[a-z]+)+$/, "arm-aaa-bbb", + "The package-dir should be a string that starts with 'arm' and is followed by one or more groups of a hyphen (-) and lowercase letters", ], [ "tspconfig-ts-mgmt-modular-package-name-match-pattern", - emitters.ts, "packageDetails.name", /^\@azure\/arm(?:-[a-z]+)+$/, "@azure/arm-aaa-bbb", + "The package name should be a string that starts with '@azure/arm' and is followed by one or more groups of a hyphen (-) and lowercase letters", + ], + // go + [ + "tspconfig-go-mgmt-service-dir-match-pattern", + "service-dir", + /^sdk\/resourcemanager\/[^\/]*$/, + "sdk/resourcemanager/aaa", + "The service-dir should be a string that start with 'sdk/resourcemanager/' followed by any characters except '/', and end there", + ], + [ + "tspconfig-go-mgmt-package-dir-match-pattern", + "package-dir", + /^arm[^\/]*$/, + "armaaa", + "The package-dir should be a string that start with 'arm' and do not contain a forward slash (/) after it", + ], + [ + "tspconfig-go-mgmt-module-equal-string", + "module", + "github.com/Azure/azure-sdk-for-go/{service-dir}/{package-dir}", + "github.com/Azure/azure-sdk-for-go/{service-dir}/{package-dir}", + undefined, ], + ["tspconfig-go-mgmt-fix-const-stuttering-true", "fix-const-stuttering", true, true, undefined], + ["tspconfig-go-mgmt-generate-examples-true", "generate-examples", true, true, undefined], + ["tspconfig-go-mgmt-generate-fakes-true", "generate-fakes", true, true, undefined], + ["tspconfig-go-mgmt-head-as-boolean-true", "head-as-boolean", true, true, undefined], + ["tspconfig-go-mgmt-inject-spans-true", "inject-spans", true, true, undefined], ]; export default args.map((a) => createManagementClientRule(...a)); diff --git a/eng/tools/eslint-plugin-tsv/src/utils/config-interpolation.ts b/eng/tools/eslint-plugin-tsv/src/utils/config-interpolation.ts new file mode 100644 index 000000000000..cd7113383d03 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/utils/config-interpolation.ts @@ -0,0 +1,221 @@ +import { + ConfigParameter, + EmitterOptions, + ConfigEnvironmentVariable, + TypeSpecConfig, +} from "../config/types.js"; + +// dummy +interface Diagnostic {} +type DiagnosticResult = [T, readonly Diagnostic[]]; +const NoTarget = 0; +function createDiagnosticCollector() { + return { + pipe: (x: DiagnosticResult): T => { + return x[0]; + }, + wrap: (x: T): [T, [Diagnostic]] => { + return [x, [{}]]; + }, + }; +} +function ignoreDiagnostics(result: DiagnosticResult): T { + return result[0]; +} +function createDiagnostic(diag: any): Diagnostic { + return {}; +} +// + +// Copied from https://github.com/microsoft/typespec/blob/main/packages/compiler/src/config/config-interpolation.ts +export interface ExpandConfigOptions { + readonly cwd: string; + readonly outputDir?: string; + readonly env?: Record; + readonly args?: Record; +} + +export function expandConfigVariables( + config: TypeSpecConfig, + expandOptions: ExpandConfigOptions, +): [TypeSpecConfig, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + const builtInVars = { + "project-root": config.projectRoot, + cwd: expandOptions.cwd, + }; + + const resolvedArgsParameters = diagnostics.pipe( + resolveArgs(config.parameters, expandOptions.args, builtInVars), + ); + const commonVars = { + ...builtInVars, + ...resolvedArgsParameters, + ...diagnostics.pipe(resolveArgs(config.options, {}, resolvedArgsParameters)), + env: diagnostics.pipe( + resolveArgs(config.environmentVariables, expandOptions.env, builtInVars, true), + ), + }; + const outputDir = diagnostics.pipe( + resolveValue(expandOptions.outputDir ?? config.outputDir, commonVars), + ); + + const result = { ...config, outputDir }; + if (config.options) { + const options: Record = {}; + for (const [name, emitterOptions] of Object.entries(config.options)) { + const emitterVars = { ...commonVars, "output-dir": outputDir, "emitter-name": name }; + options[name] = diagnostics.pipe(resolveValues(emitterOptions, emitterVars)); + } + result.options = options; + } + + return diagnostics.wrap(result); +} + +function resolveArgs( + declarations: + | Record + | undefined, + args: Record | undefined, + predefinedVariables: Record>, + allowUnspecified = false, +): [Record, readonly Diagnostic[]] { + function tryGetValue(value: any): string | undefined { + return typeof value === "string" ? value : undefined; + } + const unmatchedArgs = new Set(Object.keys(args ?? {})); + const result: Record = {}; + + function resolveNestedArgs( + parentName: string, + declarations: [string, ConfigParameter | ConfigEnvironmentVariable | EmitterOptions][], + ) { + for (const [declarationName, definition] of declarations) { + const name = parentName ? `${parentName}.${declarationName}` : declarationName; + if (hasNestedValues(definition)) { + resolveNestedArgs(name, Object.entries(definition ?? {})); + } + unmatchedArgs.delete(name); + result[name] = ignoreDiagnostics( + resolveValue( + args?.[name] ?? tryGetValue(definition.default) ?? tryGetValue(definition) ?? "", + predefinedVariables, + ), + ); + } + } + + if (declarations !== undefined) { + resolveNestedArgs("", Object.entries(declarations ?? {})); + } + + if (!allowUnspecified) { + const diagnostics: Diagnostic[] = [...unmatchedArgs].map((unmatchedArg) => { + return createDiagnostic({ + code: "config-invalid-argument", + format: { name: unmatchedArg }, + target: NoTarget, + }); + }); + return [result, diagnostics]; + } + return [result, []]; +} + +function hasNestedValues(value: any): boolean { + return ( + value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0 + ); +} + +const VariableInterpolationRegex = /{([a-zA-Z-_.]+)}/g; + +function resolveValue( + value: string, + predefinedVariables: Record>, +): [string, readonly Diagnostic[]] { + const [result, diagnostics] = resolveValues({ value }, predefinedVariables); + return [result.value, diagnostics]; +} + +export function resolveValues>( + values: T, + predefinedVariables: Record> = {}, +): [T, readonly Diagnostic[]] { + const diagnostics: Diagnostic[] = []; + const resolvedValues: Record = {}; + const resolvingValues = new Set(); + + function resolveValue(keys: string[]): unknown { + resolvingValues.add(keys[0]); + let value: any = values; + value = keys.reduce((acc, key) => acc?.[key], value); + + if (typeof value !== "string") { + if (hasNestedValues(value)) { + value = value as Record; + const resultObject: Record = {}; + for (const [nestedKey] of Object.entries(value)) { + resolvingValues.add(nestedKey); + resultObject[nestedKey] = resolveValue(keys.concat(nestedKey)) as any; + } + return resultObject; + } + return value; + } + return value.replace(VariableInterpolationRegex, (match, expression) => { + return (resolveExpression(expression) as string) ?? `{${expression}}`; + }); + } + + function resolveExpression(expression: string): unknown | undefined { + if (expression in resolvedValues) { + return resolvedValues[expression]; + } + + if (resolvingValues.has(expression)) { + diagnostics.push( + createDiagnostic({ + code: "config-circular-variable", + target: NoTarget, + format: { name: expression }, + }), + ); + return undefined; + } + + if (expression in values) { + return resolveValue([expression]) as any; + } + + let resolved: any = predefinedVariables; + if (expression in resolved) { + return resolved[expression]; + } + + const segments = expression.split("."); + for (const segment of segments) { + resolved = resolved[segment]; + if (resolved === undefined) { + return undefined; + } + } + + if (typeof resolved === "string") { + return resolved; + } else { + return undefined; + } + } + + for (const key of Object.keys(values)) { + resolvingValues.clear(); + if (key in resolvedValues) { + continue; + } + resolvedValues[key] = resolveValue([key]) as any; + } + + return [resolvedValues as any, diagnostics]; +} diff --git a/eng/tools/eslint-plugin-tsv/src/utils/rule.ts b/eng/tools/eslint-plugin-tsv/src/utils/rule.ts index 8de790b788a1..c3d1cd5972af 100644 --- a/eng/tools/eslint-plugin-tsv/src/utils/rule.ts +++ b/eng/tools/eslint-plugin-tsv/src/utils/rule.ts @@ -5,16 +5,28 @@ import { defaultMessageId, defaultRuleType, emitters } from "./constants.js"; import { NamedRule } from "../interfaces/named-eslint.js"; import { AST, getStaticYAMLValue } from "yaml-eslint-parser"; import { stringify } from "yaml"; +import { resolveValues } from "./config-interpolation.js"; function createManagementRuleDocument( emitterName: string, optionName: string, expectedOptionValue: string | boolean | RegExp, exampleValue: string | boolean, + extraExplanation: string, ): RuleDocument { + let description: string = ""; + switch (typeof expectedOptionValue) { + case "string": + case "boolean": + `Validate whether 'options.${emitters.ts}.${optionName}' is set to '${expectedOptionValue}' in tspconfig.yaml when generating modular clients`; + break; + case "object": + `Validate whether 'options.${emitters.ts}.${optionName}' matches regex pattern '${expectedOptionValue}' in tspconfig.yaml when generating modular clients. ${extraExplanation}`; + break; + } + const document: RuleDocument = { - // TODO: improve description with natual language - description: `Validate whether 'options.${emitters.ts}.${optionName}' is set to '${expectedOptionValue}' in tspconfig.yaml when generating modular clients`, + description, error: `'options.${emitters.ts}.${optionName}' is NOT set to '${expectedOptionValue}' in tspconfig.yaml when generating modular clients`, action: `Set 'options.${emitters.ts}.${optionName}' to '${expectedOptionValue}' in tspconfig.yaml when generating modular clients`, example: createEmitterOptions(emitterName, { key: optionName, value: exampleValue }), @@ -37,10 +49,11 @@ export function createRule(ruleContext: RuleInfo): NamedRule.RuleModule { return { YAMLDocument(node: Rule.Node) { const yamlDocument = node as unknown as AST.YAMLDocument; - const config = getStaticYAMLValue(yamlDocument) || {}; - const typedConfig = config as unknown as TypeSpecConfig; - if (!ruleContext.functions.condition(typedConfig, context)) return; - ruleContext.functions.validation(typedConfig, context, node); + const rawConfig = getStaticYAMLValue(yamlDocument) || {}; + const config = rawConfig as unknown as TypeSpecConfig; + + if (!ruleContext.functions.condition(config, context)) return; + ruleContext.functions.validation(config, context, node); }, }; }, @@ -54,13 +67,14 @@ export function createRuleMessages(messageId: string, docs: RuleDocument) { }; } -export function isManagementForTsEmitter(tspconfig: TypeSpecConfig, context: Rule.RuleContext) { - const flavor = tspconfig.options?.[emitters.ts]?.flavor as string; - const isModularLibrary = tspconfig.options?.[emitters.ts]?.isModularLibrary as - | boolean - | undefined; +export function isManagementSDK( + tspconfig: TypeSpecConfig, + context: Rule.RuleContext, + emitterName: string, +) { + const flavor = tspconfig.options?.[emitterName]?.flavor as string; const filename = context.filename; - return flavor === "azure" && filename.includes(".Management") && isModularLibrary == undefined; + return flavor === "azure" && filename.includes(".Management"); } export function createEmitterOptions( @@ -88,16 +102,19 @@ export function createEmitterOptions( export function createManagementClientRule( ruleName: string, - emitterName: string, optionName: string, expectedOptionValue: string | boolean | RegExp, exampleValue: string | boolean, + extraExplanation: string = "", ): NamedRule.RuleModule { + const language = ruleName.split("-")[1]! as keyof typeof emitters; + const emitterName = emitters[language]; const documentation = createManagementRuleDocument( emitterName, optionName, expectedOptionValue, exampleValue, + extraExplanation, ); const ruleInfo: RuleInfo = { @@ -105,9 +122,20 @@ export function createManagementClientRule( documentation, functions: { messages: () => createRuleMessages(defaultMessageId, documentation), - condition: (tspconfig, context) => isManagementForTsEmitter(tspconfig, context), + condition: (tspconfig, context) => { + const isManagement = isManagementSDK(tspconfig, context, emitterName); + if (!isManagement) return false; + + if (emitterName === emitters.ts) { + const isModularLibrary = tspconfig.options?.[emitters.ts]?.isModularLibrary as + | boolean + | undefined; + return isModularLibrary === undefined || isModularLibrary === true; + } + return true; + }, validation: (tspconfig, context, node) => { - let option: any = tspconfig.options?.[emitters.ts]; + let option: any = tspconfig.options?.[emitterName]; for (const segment of optionName.split(".")) { if (segment in option) option = option[segment]; } diff --git a/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts b/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts index 2b1c1f1ecd5c..47506385e566 100644 --- a/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts @@ -17,7 +17,7 @@ interface Case { const managementTspconfigPath = "contosowidgetmanager/Contoso.Management/tspconfig.yaml"; const rulePath = "../../src/rules/tspconfig-validation-rules.js"; -const managementGenerateMetadataTestCases = generateManagementClientTestCases( +const tsManagementGenerateMetadataTestCases = generateManagementClientTestCases( emitters.ts, rulePath, "tspconfig-ts-mgmt-modular-generate-metadata-true", @@ -27,7 +27,7 @@ const managementGenerateMetadataTestCases = generateManagementClientTestCases( false, ); -const managementHierarchyClientTestCases = generateManagementClientTestCases( +const tsManagementHierarchyClientTestCases = generateManagementClientTestCases( emitters.ts, rulePath, "tspconfig-ts-mgmt-modular-hierarchy-client-false", @@ -37,7 +37,7 @@ const managementHierarchyClientTestCases = generateManagementClientTestCases( true, ); -const managementExperimentalExtensibleEnumsTestCases = generateManagementClientTestCases( +const tsManagementExperimentalExtensibleEnumsTestCases = generateManagementClientTestCases( emitters.ts, rulePath, "tspconfig-ts-mgmt-modular-experimental-extensible-enums-true", @@ -47,7 +47,7 @@ const managementExperimentalExtensibleEnumsTestCases = generateManagementClientT false, ); -const managementEnableOperationGroupTestCases = generateManagementClientTestCases( +const tsManagementEnableOperationGroupTestCases = generateManagementClientTestCases( emitters.ts, rulePath, "tspconfig-ts-mgmt-modular-enable-operation-group-true", @@ -57,7 +57,7 @@ const managementEnableOperationGroupTestCases = generateManagementClientTestCase false, ); -const managementPackageDirTestCases = generateManagementClientTestCases( +const tsManagementPackageDirTestCases = generateManagementClientTestCases( emitters.ts, rulePath, "tspconfig-ts-mgmt-modular-package-dir-match-pattern", @@ -67,7 +67,7 @@ const managementPackageDirTestCases = generateManagementClientTestCases( "aaa-bbb", ); -const managementPackageNameTestCases = generateManagementClientTestCases( +const tsManagementPackageNameTestCases = generateManagementClientTestCases( emitters.ts, rulePath, "tspconfig-ts-mgmt-modular-package-name-match-pattern", @@ -77,6 +77,86 @@ const managementPackageNameTestCases = generateManagementClientTestCases( "@azure/aaa-bbb", ); +const goManagementServiceDirTestCases = generateManagementClientTestCases( + emitters.go, + rulePath, + "tspconfig-go-mgmt-service-dir-match-pattern", + managementTspconfigPath, + "service-dir", + "sdk/resourcemanager/aaa", + "sdk/manager/aaa", +); + +const goManagementPackageDirTestCases = generateManagementClientTestCases( + emitters.go, + rulePath, + "tspconfig-go-mgmt-package-dir-match-pattern", + managementTspconfigPath, + "package-dir", + "armaaa", + "aaa", +); + +const goManagementModuleTestCases = generateManagementClientTestCases( + emitters.go, + rulePath, + "tspconfig-go-mgmt-module-equal-string", + managementTspconfigPath, + "module", + "github.com/Azure/azure-sdk-for-go/{service-dir}/{package-dir}", + "github.com/Azure/azure-sdk-for-java/{service-dir}/{package-dir}", +); + +const goManagementFixConstStutteringTestCases = generateManagementClientTestCases( + emitters.go, + rulePath, + "tspconfig-go-mgmt-fix-const-stuttering-true", + managementTspconfigPath, + "fix-const-stuttering", + true, + false, +); + +const goManagementGenerateExamplesTestCases = generateManagementClientTestCases( + emitters.go, + rulePath, + "tspconfig-go-mgmt-generate-examples-true", + managementTspconfigPath, + "generate-examples", + true, + false, +); + +const goManagementGenerateFakesTestCases = generateManagementClientTestCases( + emitters.go, + rulePath, + "tspconfig-go-mgmt-generate-fakes-true", + managementTspconfigPath, + "generate-fakes", + true, + false, +); + +const goManagementHeadAsBooleanTestCases = generateManagementClientTestCases( + emitters.go, + rulePath, + "tspconfig-go-mgmt-head-as-boolean-true", + managementTspconfigPath, + "head-as-boolean", + true, + false, +); + +const goManagementInjectSpansTestCases = generateManagementClientTestCases( + emitters.go, + rulePath, + "tspconfig-go-mgmt-inject-spans-true", + managementTspconfigPath, + "inject-spans", + true, + false, +); + function generateManagementClientTestCases( emitterName: string, rulePath: string, @@ -125,12 +205,22 @@ function generateManagementClientTestCases( describe("Tspconfig emitter options validation", () => { it.each([ - ...managementGenerateMetadataTestCases, - ...managementHierarchyClientTestCases, - ...managementExperimentalExtensibleEnumsTestCases, - ...managementEnableOperationGroupTestCases, - ...managementPackageDirTestCases, - ...managementPackageNameTestCases, + // ts + ...tsManagementGenerateMetadataTestCases, + ...tsManagementHierarchyClientTestCases, + ...tsManagementExperimentalExtensibleEnumsTestCases, + ...tsManagementEnableOperationGroupTestCases, + ...tsManagementPackageDirTestCases, + ...tsManagementPackageNameTestCases, + // go + ...goManagementServiceDirTestCases, + ...goManagementPackageDirTestCases, + ...goManagementModuleTestCases, + ...goManagementFixConstStutteringTestCases, + ...goManagementGenerateExamplesTestCases, + ...goManagementGenerateFakesTestCases, + ...goManagementHeadAsBooleanTestCases, + ...goManagementInjectSpansTestCases, ])("$ruleName - $description", async (c: Case) => { const ruleTester = new RuleTester({ languageOptions: { From 887282be8b2ab80977a8edc63ee46399e96f6867 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Wed, 22 Jan 2025 18:05:58 +0800 Subject: [PATCH 65/93] added all rules! : ) --- .../src/interfaces/rule-interfaces.ts | 15 + .../src/rules/tspconfig-validation-rules.ts | 307 ++++++++++++++---- .../src/utils/rule-creator.ts | 128 ++++++++ .../eslint-plugin-tsv/src/utils/rule-doc.ts | 114 +++++++ eng/tools/eslint-plugin-tsv/src/utils/rule.ts | 158 --------- .../tspconfig-options-validation.test.ts | 270 +++++++++++---- 6 files changed, 715 insertions(+), 277 deletions(-) create mode 100644 eng/tools/eslint-plugin-tsv/src/utils/rule-creator.ts create mode 100644 eng/tools/eslint-plugin-tsv/src/utils/rule-doc.ts delete mode 100644 eng/tools/eslint-plugin-tsv/src/utils/rule.ts diff --git a/eng/tools/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts b/eng/tools/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts index 4ea789d7ba86..d45d6ab20597 100644 --- a/eng/tools/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts +++ b/eng/tools/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts @@ -1,6 +1,11 @@ import { Rule } from "eslint"; import { TypeSpecConfig } from "../config/types.js"; +export enum KeyType { + EmitterOption, + Parameter, +} + export interface RuleDocument { description: string; error: string; @@ -17,3 +22,13 @@ export interface RuleInfo { validation: (tspconfig: TypeSpecConfig, context: Rule.RuleContext, node: Rule.Node) => void; }; } + +export interface CreateCodeGenSDKRuleArgs { + rule: string; + type: KeyType; + key: string; + expectedValue: string | boolean | RegExp; + exampleValue: string | boolean; + extraExplanation?: string; + condition?: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => boolean; +} \ No newline at end of file diff --git a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts index adf88d940aed..719e370ac560 100644 --- a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts +++ b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts @@ -1,64 +1,253 @@ -import { createManagementClientRule } from "../utils/rule.js"; +import { Rule } from "eslint"; +import { TypeSpecConfig } from "../config/types.js"; +import { + createCodeGenSDKRule, + isAzureSDK, + isManagementSDK, +} from "../utils/rule-creator.js"; +import { emitters } from "../utils/constants.js"; +import { CreateCodeGenSDKRuleArgs, KeyType } from "../interfaces/rule-interfaces.js"; -const args: [string, string, string | boolean | RegExp, string | boolean, string | undefined][] = [ +const tsIsManagementCondition = (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => { + const emitterName = emitters.ts; + const isModularLibrary = tspconfig.options?.[emitterName]?.isModularLibrary as + | boolean + | undefined; + return isManagementSDK(tspconfig, context, emitterName) && isModularLibrary !== false; +}; + +const args: CreateCodeGenSDKRuleArgs[] = [ + // common + { + rule: "tspconfig-common-az-service-dir-match-pattern", + key: "service-dir", + type: KeyType.Parameter, + expectedValue: /^sdk\/[^\/]*$/, + exampleValue: "sdk/aaa", + extraExplanation: + "The 'service-dir' should be a string that starts with 'sdk/', followed by zero or more characters that are not a '/', and ends there", + condition: (_: TypeSpecConfig, _1: Rule.RuleContext) => true, + }, // ts - ["tspconfig-ts-mgmt-modular-generate-metadata-true", "generateMetadata", true, true, undefined], - ["tspconfig-ts-mgmt-modular-hierarchy-client-false", "hierarchyClient", false, false, undefined], - [ - "tspconfig-ts-mgmt-modular-experimental-extensible-enums-true", - "experimentalExtensibleEnums", - true, - true, - undefined, - ], - [ - "tspconfig-ts-mgmt-modular-enable-operation-group-true", - "enableOperationGroup", - true, - true, - undefined, - ], - [ - "tspconfig-ts-mgmt-modular-package-dir-match-pattern", - "package-dir", - /^arm(?:-[a-z]+)+$/, - "arm-aaa-bbb", - "The package-dir should be a string that starts with 'arm' and is followed by one or more groups of a hyphen (-) and lowercase letters", - ], - [ - "tspconfig-ts-mgmt-modular-package-name-match-pattern", - "packageDetails.name", - /^\@azure\/arm(?:-[a-z]+)+$/, - "@azure/arm-aaa-bbb", - "The package name should be a string that starts with '@azure/arm' and is followed by one or more groups of a hyphen (-) and lowercase letters", - ], + { + rule: "tspconfig-ts-mgmt-modular-generate-metadata-true", + key: "generateMetadata", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: tsIsManagementCondition, + }, + { + rule: "tspconfig-ts-mgmt-modular-hierarchy-client-false", + key: "hierarchyClient", + type: KeyType.EmitterOption, + expectedValue: false, + exampleValue: false, + condition: tsIsManagementCondition, + }, + { + rule: "tspconfig-ts-mgmt-modular-experimental-extensible-enums-true", + key: "experimentalExtensibleEnums", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: tsIsManagementCondition, + }, + { + rule: "tspconfig-ts-mgmt-modular-enable-operation-group-true", + key: "enableOperationGroup", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: tsIsManagementCondition, + }, + { + rule: "tspconfig-ts-mgmt-modular-package-dir-match-pattern", + key: "package-dir", + type: KeyType.EmitterOption, + expectedValue: /^arm(?:-[a-z]+)+$/, + exampleValue: "arm-aaa-bbb", + extraExplanation: + "The 'package-dir' should be a string that starts with 'arm' and is followed by one or more groups of a hyphen (-) and lowercase letters", + condition: tsIsManagementCondition, + }, + { + rule: "tspconfig-ts-mgmt-modular-package-name-match-pattern", + key: "packageDetails.name", + type: KeyType.EmitterOption, + expectedValue: /^\@azure\/arm(?:-[a-z]+)+$/, + exampleValue: "@azure/arm-aaa-bbb", + extraExplanation: + "The package name should be a string that starts with '@azure/arm' and is followed by one or more groups of a hyphen (-) and lowercase letters", + condition: tsIsManagementCondition, + }, // go - [ - "tspconfig-go-mgmt-service-dir-match-pattern", - "service-dir", - /^sdk\/resourcemanager\/[^\/]*$/, - "sdk/resourcemanager/aaa", - "The service-dir should be a string that start with 'sdk/resourcemanager/' followed by any characters except '/', and end there", - ], - [ - "tspconfig-go-mgmt-package-dir-match-pattern", - "package-dir", - /^arm[^\/]*$/, - "armaaa", - "The package-dir should be a string that start with 'arm' and do not contain a forward slash (/) after it", - ], - [ - "tspconfig-go-mgmt-module-equal-string", - "module", - "github.com/Azure/azure-sdk-for-go/{service-dir}/{package-dir}", - "github.com/Azure/azure-sdk-for-go/{service-dir}/{package-dir}", - undefined, - ], - ["tspconfig-go-mgmt-fix-const-stuttering-true", "fix-const-stuttering", true, true, undefined], - ["tspconfig-go-mgmt-generate-examples-true", "generate-examples", true, true, undefined], - ["tspconfig-go-mgmt-generate-fakes-true", "generate-fakes", true, true, undefined], - ["tspconfig-go-mgmt-head-as-boolean-true", "head-as-boolean", true, true, undefined], - ["tspconfig-go-mgmt-inject-spans-true", "inject-spans", true, true, undefined], + { + rule: "tspconfig-go-mgmt-service-dir-match-pattern", + key: "service-dir", + type: KeyType.EmitterOption, + expectedValue: /^sdk\/resourcemanager\/[^\/]*$/, + exampleValue: "sdk/resourcemanager/aaa", + extraExplanation: + "The 'service-dir' should be a string that starts with 'sdk/resourcemanager/', followed by zero or more characters that are not a '/', and ends there", + condition: (_: TypeSpecConfig, _1: Rule.RuleContext) => true, + }, + { + rule: "tspconfig-go-mgmt-package-dir-match-pattern", + key: "package-dir", + type: KeyType.EmitterOption, + expectedValue: /^arm[^\/]*$/, + exampleValue: "armaaa", + extraExplanation: + "The 'package-dir' should be a string that starts with 'arm' and do not contain a forward slash (/) after it", + condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => + isManagementSDK(tspconfig, context, emitters.go), + }, + { + rule: "tspconfig-go-mgmt-module-equal-string", + key: "module", + type: KeyType.EmitterOption, + expectedValue: "github.com/Azure/azure-sdk-for-go/{service-dir}/{package-dir}", + exampleValue: "github.com/Azure/azure-sdk-for-go/{service-dir}/{package-dir}", + condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => + isManagementSDK(tspconfig, context, emitters.go), + }, + { + rule: "tspconfig-go-mgmt-fix-const-stuttering-true", + key: "fix-const-stuttering", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => + isManagementSDK(tspconfig, context, emitters.go), + }, + { + rule: "tspconfig-go-mgmt-generate-examples-true", + key: "generate-examples", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => + isManagementSDK(tspconfig, context, emitters.go), + }, + { + rule: "tspconfig-go-mgmt-generate-fakes-true", + key: "generate-fakes", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => + isManagementSDK(tspconfig, context, emitters.go), + }, + { + rule: "tspconfig-go-mgmt-head-as-boolean-true", + key: "head-as-boolean", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => + isManagementSDK(tspconfig, context, emitters.go), + }, + { + rule: "tspconfig-go-mgmt-inject-spans-true", + key: "inject-spans", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => + isManagementSDK(tspconfig, context, emitters.go), + }, + // java + { + rule: "tspconfig-java-mgmt-package-dir-match-pattern", + key: "package-dir", + type: KeyType.EmitterOption, + expectedValue: /^azure(-\w+)+$/, + exampleValue: "azure-aaa", + extraExplanation: + "The 'package-dir' should be a string that starts with 'azure', followed by one or more '-' segments. Each segment can contains letters, digits, or underscores", + condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => + isManagementSDK(tspconfig, context, emitters.java), + }, + // python + { + rule: "tspconfig-python-mgmt-package-dir-match-pattern", + key: "package-dir", + type: KeyType.EmitterOption, + expectedValue: /^azure-mgmt(-[a-z]+){1,2}$/, + exampleValue: "azure-mgmt-aaa", + extraExplanation: + "The 'package-dir' should be a string that starts with 'azure-mgmt', followed by 1 or 2 hyphen-separated lowercase alphabetic segments", + condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => + isManagementSDK(tspconfig, context, emitters.python), + }, + { + rule: "tspconfig-python-mgmt-package-name-equal-string", + key: "package-name", + type: KeyType.EmitterOption, + expectedValue: "{package-dir}", + exampleValue: "{package-dir}", + condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => + isManagementSDK(tspconfig, context, emitters.python), + }, + { + rule: "tspconfig-python-mgmt-generate-test-true", + key: "generate-test", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => + isManagementSDK(tspconfig, context, emitters.python), + }, + { + rule: "tspconfig-python-mgmt-generate-sample-true", + key: "generate-sample", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => + isManagementSDK(tspconfig, context, emitters.python), + }, + // csharp + { + rule: "tspconfig-csharp-az-package-dir-match-pattern", + key: "package-dir", + type: KeyType.EmitterOption, + expectedValue: /^Azure\./, + exampleValue: "Azure.aaa", + extraExplanation: "The 'package-dir' should be a string that starts with 'Azure.'", + condition: (tspconfig: TypeSpecConfig, _: Rule.RuleContext) => + isAzureSDK(tspconfig, emitters.csharp), + }, + { + rule: "tspconfig-csharp-az-namespace-equal-string", + key: "namespace", + type: KeyType.EmitterOption, + expectedValue: "{package-dir}", + exampleValue: "{package-dir}", + condition: (tspconfig: TypeSpecConfig, _: Rule.RuleContext) => + isAzureSDK(tspconfig, emitters.csharp), + }, + { + rule: "tspconfig-csharp-az-clear-output-folder-true", + key: "clear-output-folder", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: (tspconfig: TypeSpecConfig, _: Rule.RuleContext) => + isAzureSDK(tspconfig, emitters.csharp), + }, + { + rule: "tspconfig-csharp-mgmt-package-dir-match-pattern", + key: "package-dir", + type: KeyType.EmitterOption, + expectedValue: /^Azure\.ResourceManager\./, + exampleValue: "Azure.ResourceManager.aaa", + extraExplanation: + "The 'package-dir' should be a string that starts with 'Azure.ResourceManager.'", + condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => + isManagementSDK(tspconfig, context, emitters.csharp), + }, ]; -export default args.map((a) => createManagementClientRule(...a)); +export default args.map((a) => createCodeGenSDKRule(a)); diff --git a/eng/tools/eslint-plugin-tsv/src/utils/rule-creator.ts b/eng/tools/eslint-plugin-tsv/src/utils/rule-creator.ts new file mode 100644 index 000000000000..ab921cbd8a07 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/utils/rule-creator.ts @@ -0,0 +1,128 @@ +import { Rule } from "eslint"; +import { TypeSpecConfig } from "../config/types.js"; +import { CreateCodeGenSDKRuleArgs, KeyType, RuleDocument, RuleInfo } from "../interfaces/rule-interfaces.js"; +import { defaultMessageId, defaultRuleType, emitters } from "./constants.js"; +import { NamedRule } from "../interfaces/named-eslint.js"; +import { AST, getStaticYAMLValue } from "yaml-eslint-parser"; +import { createRuleDocument } from "./rule-doc.js"; + +export function createRule(ruleContext: RuleInfo): NamedRule.RuleModule { + const rule: NamedRule.RuleModule = { + name: ruleContext.name, + meta: { + type: defaultRuleType, + docs: { + description: ruleContext.documentation.description, + }, + schema: [], + messages: ruleContext.functions.messages(), + }, + create(context) { + return { + YAMLDocument(node: Rule.Node) { + const yamlDocument = node as unknown as AST.YAMLDocument; + const rawConfig = getStaticYAMLValue(yamlDocument) || {}; + const config = rawConfig as unknown as TypeSpecConfig; + + if (!ruleContext.functions.condition(config, context)) return; + ruleContext.functions.validation(config, context, node); + }, + }; + }, + }; + return rule; +} + +export function createRuleMessages(messageId: string, docs: RuleDocument) { + return { + [messageId]: `${docs.error}.\n${docs.action}.\n${docs.example}`, + }; +} + +export function isAzureSDK(tspconfig: TypeSpecConfig, emitterName: string) { + const flavor = tspconfig.options?.[emitterName]?.flavor as string; + return flavor === "azure"; +} + +export function isManagementSDK( + tspconfig: TypeSpecConfig, + context: Rule.RuleContext, + emitterName: string, +) { + const filename = context.filename; + return isAzureSDK(tspconfig, emitterName) && filename.includes(".Management"); +} + +function validateValue( + context: Rule.RuleContext, + node: Rule.Node, + actual: string | boolean | undefined, + expected: boolean | string | RegExp, +) { + switch (typeof expected) { + case "boolean": + case "string": + if (actual !== expected) + context.report({ node, messageId: defaultMessageId }); + break; + case "object": + if (typeof actual !== "string" || !expected.test(actual)) + context.report({ node, messageId: defaultMessageId }); + break; + case "undefined": + context.report({ node, messageId: defaultMessageId }); + break; + default: + // TODO: log not supported + break; + } +} + +// TODO: add logs +export function createCodeGenSDKRule(args: CreateCodeGenSDKRuleArgs): NamedRule.RuleModule { + const language = args.rule.split("-")[1]! as keyof typeof emitters; + const emitterName = emitters[language]; + const documentation = createRuleDocument( + emitterName, + args.type, + args.key, + args.expectedValue, + args.exampleValue, + args.extraExplanation ?? "", + ); + + const ruleInfo: RuleInfo = { + name: args.rule, + documentation: documentation!, + functions: { + messages: () => createRuleMessages(defaultMessageId, documentation), + condition: (tspconfig, context) => { + if (args.condition) return args.condition(tspconfig, context); + return true; + }, + validation: (tspconfig, context, node) => { + switch (args.type) { + case KeyType.EmitterOption: { + let option: Record | undefined = tspconfig.options?.[emitterName]; + for (const segment of args.key.split(".")) { + if (option && segment in option) option = option![segment]; + } + validateValue(context, node, option as undefined | string | boolean, args.expectedValue); + break; + } + case KeyType.Parameter: { + const parameter = tspconfig.parameters?.[args.key].default; + validateValue(context, node, parameter, args.expectedValue); + break; + } + default: + // TODO: log not supported + break; + } + + }, + }, + }; + + return createRule(ruleInfo); +} diff --git a/eng/tools/eslint-plugin-tsv/src/utils/rule-doc.ts b/eng/tools/eslint-plugin-tsv/src/utils/rule-doc.ts new file mode 100644 index 000000000000..f1525db38605 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/utils/rule-doc.ts @@ -0,0 +1,114 @@ +import { KeyType, RuleDocument } from "../interfaces/rule-interfaces.js"; +import { emitters } from "./constants.js"; +import { stringify } from "yaml"; + +function createDescriptionDocumentBlock( + displayName: string, + expectedValue: string | boolean | RegExp, + extraExplanation: string, +): string { + switch (typeof expectedValue) { + case "object": + return `Validate whether '${displayName}' matches regex pattern '${expectedValue}' in tspconfig.yaml. ${extraExplanation}`; + default: + case "string": + case "boolean": + return `Validate whether '${displayName}' is set to '${expectedValue}' in tspconfig.yaml`; + } +} + +function createErrorDocumentBlock( + displayName: string, + expectedValue: string | boolean | RegExp, +): string { + switch (typeof expectedValue) { + case "object": + return `'${displayName}' does NOT match regex pattern '${expectedValue}' in tspconfig.yaml`; + default: + case "string": + case "boolean": + return `'${displayName}' is NOT set to '${expectedValue}' in tspconfig.yaml`; + } +} + +function createActionDocumentBlock( + displayName: string, + expectedValue: string | boolean | RegExp, +): string { + switch (typeof expectedValue) { + case "object": + return `Set '${displayName}' to a value that matches regex pattern '${expectedValue}' in tspconfig.yaml`; + default: + case "string": + case "boolean": + return `Set '${displayName}' to '${expectedValue}' in tspconfig.yaml`; + } +} + +export function createRuleDocument( + emitterName: string, + keyType: KeyType, + key: string, + expectedValue: string | boolean | RegExp, + exampleValue: string | boolean, + extraExplanation: string, +): RuleDocument { + let displayName = key; + let example = ""; + switch (keyType) { + case KeyType.EmitterOption: + displayName = `options.${emitters.ts}.${key}`; + example = createEmitterOptionExample(emitterName, { key: key, value: exampleValue }); + break; + case KeyType.Parameter: + displayName = `parameters.${key}`; + example = createParameterExample({ key: key, value: exampleValue }); + break; + default: + // TODO: log not supported + displayName = key; + } + const description = createDescriptionDocumentBlock(displayName, expectedValue, extraExplanation); + const error = createErrorDocumentBlock(displayName, expectedValue); + const action = createActionDocumentBlock(displayName, expectedValue); + + const document: RuleDocument = { + description, + error, + action, + example, + }; + return document; +} + +export function createParameterExample(...pairs: { key: string; value: string | boolean | {} }[]) { + const obj: Record = { parameters: {} }; + for (const pair of pairs) { + obj.parameters[pair.key] = { default: pair.value }; + } + const content = stringify(obj); + return content; +} + +export function createEmitterOptionExample( + emitter: string, + ...pairs: { key: string; value: string | boolean | {} }[] +) { + const obj = { options: { [emitter]: {} } }; + for (const pair of pairs) { + const segments = pair.key.split("."); + let cur: Record = obj.options[emitter]; + for (const [i, segment] of segments.entries()) { + if (i === segments.length - 1) { + cur[segment] = pair.value; + break; + } + if (!(segment in cur)) { + cur[segment] = {}; + } + cur = cur[segment]; + } + } + const content = stringify(obj); + return content; +} diff --git a/eng/tools/eslint-plugin-tsv/src/utils/rule.ts b/eng/tools/eslint-plugin-tsv/src/utils/rule.ts deleted file mode 100644 index c3d1cd5972af..000000000000 --- a/eng/tools/eslint-plugin-tsv/src/utils/rule.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { Rule } from "eslint"; -import { TypeSpecConfig } from "../config/types.js"; -import { RuleDocument, RuleInfo } from "../interfaces/rule-interfaces.js"; -import { defaultMessageId, defaultRuleType, emitters } from "./constants.js"; -import { NamedRule } from "../interfaces/named-eslint.js"; -import { AST, getStaticYAMLValue } from "yaml-eslint-parser"; -import { stringify } from "yaml"; -import { resolveValues } from "./config-interpolation.js"; - -function createManagementRuleDocument( - emitterName: string, - optionName: string, - expectedOptionValue: string | boolean | RegExp, - exampleValue: string | boolean, - extraExplanation: string, -): RuleDocument { - let description: string = ""; - switch (typeof expectedOptionValue) { - case "string": - case "boolean": - `Validate whether 'options.${emitters.ts}.${optionName}' is set to '${expectedOptionValue}' in tspconfig.yaml when generating modular clients`; - break; - case "object": - `Validate whether 'options.${emitters.ts}.${optionName}' matches regex pattern '${expectedOptionValue}' in tspconfig.yaml when generating modular clients. ${extraExplanation}`; - break; - } - - const document: RuleDocument = { - description, - error: `'options.${emitters.ts}.${optionName}' is NOT set to '${expectedOptionValue}' in tspconfig.yaml when generating modular clients`, - action: `Set 'options.${emitters.ts}.${optionName}' to '${expectedOptionValue}' in tspconfig.yaml when generating modular clients`, - example: createEmitterOptions(emitterName, { key: optionName, value: exampleValue }), - }; - return document; -} - -export function createRule(ruleContext: RuleInfo): NamedRule.RuleModule { - const rule: NamedRule.RuleModule = { - name: ruleContext.name, - meta: { - type: defaultRuleType, - docs: { - description: ruleContext.documentation.description, - }, - schema: [], - messages: ruleContext.functions.messages(), - }, - create(context) { - return { - YAMLDocument(node: Rule.Node) { - const yamlDocument = node as unknown as AST.YAMLDocument; - const rawConfig = getStaticYAMLValue(yamlDocument) || {}; - const config = rawConfig as unknown as TypeSpecConfig; - - if (!ruleContext.functions.condition(config, context)) return; - ruleContext.functions.validation(config, context, node); - }, - }; - }, - }; - return rule; -} - -export function createRuleMessages(messageId: string, docs: RuleDocument) { - return { - [messageId]: `${docs.error}.\n${docs.action}.\n${docs.example}`, - }; -} - -export function isManagementSDK( - tspconfig: TypeSpecConfig, - context: Rule.RuleContext, - emitterName: string, -) { - const flavor = tspconfig.options?.[emitterName]?.flavor as string; - const filename = context.filename; - return flavor === "azure" && filename.includes(".Management"); -} - -export function createEmitterOptions( - emitter: string, - ...pairs: { key: string; value: string | boolean | {} }[] -) { - const obj = { options: { [emitter]: {} } }; - for (const pair of pairs) { - const segments = pair.key.split("."); - let cur: { [id: string]: any } = obj.options[emitter]; - for (const [i, segment] of segments.entries()) { - if (i === segments.length - 1) { - cur[segment] = pair.value; - break; - } - if (!(segment in cur)) { - cur[segment] = {}; - } - cur = cur[segment]; - } - } - const content = stringify(obj); - return content; -} - -export function createManagementClientRule( - ruleName: string, - optionName: string, - expectedOptionValue: string | boolean | RegExp, - exampleValue: string | boolean, - extraExplanation: string = "", -): NamedRule.RuleModule { - const language = ruleName.split("-")[1]! as keyof typeof emitters; - const emitterName = emitters[language]; - const documentation = createManagementRuleDocument( - emitterName, - optionName, - expectedOptionValue, - exampleValue, - extraExplanation, - ); - - const ruleInfo: RuleInfo = { - name: ruleName, - documentation, - functions: { - messages: () => createRuleMessages(defaultMessageId, documentation), - condition: (tspconfig, context) => { - const isManagement = isManagementSDK(tspconfig, context, emitterName); - if (!isManagement) return false; - - if (emitterName === emitters.ts) { - const isModularLibrary = tspconfig.options?.[emitters.ts]?.isModularLibrary as - | boolean - | undefined; - return isModularLibrary === undefined || isModularLibrary === true; - } - return true; - }, - validation: (tspconfig, context, node) => { - let option: any = tspconfig.options?.[emitterName]; - for (const segment of optionName.split(".")) { - if (segment in option) option = option[segment]; - } - switch (typeof expectedOptionValue) { - case "boolean": - case "string": - if (option !== expectedOptionValue) - context.report({ node, messageId: defaultMessageId }); - break; - case "object": - if (typeof option !== "string" || !expectedOptionValue.test(option)) - context.report({ node, messageId: defaultMessageId }); - break; - } - }, - }, - }; - - return createRule(ruleInfo); -} diff --git a/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts b/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts index 47506385e566..a2456cefe83f 100644 --- a/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts @@ -2,8 +2,8 @@ import { Rule, RuleTester } from "eslint"; import { describe, it } from "vitest"; import parser from "yaml-eslint-parser"; import { defaultMessageId, emitters } from "../../src/utils/constants.js"; -import { createEmitterOptions } from "../../src/utils/rule.js"; import { NamedRule } from "../../src/interfaces/named-eslint.js"; +import { createEmitterOptionExample, createParameterExample } from "../../src/utils/rule-doc.js"; interface Case { description: string; @@ -17,7 +17,16 @@ interface Case { const managementTspconfigPath = "contosowidgetmanager/Contoso.Management/tspconfig.yaml"; const rulePath = "../../src/rules/tspconfig-validation-rules.js"; -const tsManagementGenerateMetadataTestCases = generateManagementClientTestCases( +const commonAzureServiceDirTestCases = createParameterTestCases( + rulePath, + "tspconfig-common-az-service-dir-match-pattern", + "", + "service-dir", + "sdk/aaa", + "sdka/aaa", +); + +const tsManagementGenerateMetadataTestCases = createEmitterOptionTestCases( emitters.ts, rulePath, "tspconfig-ts-mgmt-modular-generate-metadata-true", @@ -27,7 +36,7 @@ const tsManagementGenerateMetadataTestCases = generateManagementClientTestCases( false, ); -const tsManagementHierarchyClientTestCases = generateManagementClientTestCases( +const tsManagementHierarchyClientTestCases = createEmitterOptionTestCases( emitters.ts, rulePath, "tspconfig-ts-mgmt-modular-hierarchy-client-false", @@ -37,7 +46,7 @@ const tsManagementHierarchyClientTestCases = generateManagementClientTestCases( true, ); -const tsManagementExperimentalExtensibleEnumsTestCases = generateManagementClientTestCases( +const tsManagementExperimentalExtensibleEnumsTestCases = createEmitterOptionTestCases( emitters.ts, rulePath, "tspconfig-ts-mgmt-modular-experimental-extensible-enums-true", @@ -47,7 +56,7 @@ const tsManagementExperimentalExtensibleEnumsTestCases = generateManagementClien false, ); -const tsManagementEnableOperationGroupTestCases = generateManagementClientTestCases( +const tsManagementEnableOperationGroupTestCases = createEmitterOptionTestCases( emitters.ts, rulePath, "tspconfig-ts-mgmt-modular-enable-operation-group-true", @@ -57,7 +66,7 @@ const tsManagementEnableOperationGroupTestCases = generateManagementClientTestCa false, ); -const tsManagementPackageDirTestCases = generateManagementClientTestCases( +const tsManagementPackageDirTestCases = createEmitterOptionTestCases( emitters.ts, rulePath, "tspconfig-ts-mgmt-modular-package-dir-match-pattern", @@ -67,7 +76,7 @@ const tsManagementPackageDirTestCases = generateManagementClientTestCases( "aaa-bbb", ); -const tsManagementPackageNameTestCases = generateManagementClientTestCases( +const tsManagementPackageNameTestCases = createEmitterOptionTestCases( emitters.ts, rulePath, "tspconfig-ts-mgmt-modular-package-name-match-pattern", @@ -77,7 +86,7 @@ const tsManagementPackageNameTestCases = generateManagementClientTestCases( "@azure/aaa-bbb", ); -const goManagementServiceDirTestCases = generateManagementClientTestCases( +const goManagementServiceDirTestCases = createEmitterOptionTestCases( emitters.go, rulePath, "tspconfig-go-mgmt-service-dir-match-pattern", @@ -87,7 +96,7 @@ const goManagementServiceDirTestCases = generateManagementClientTestCases( "sdk/manager/aaa", ); -const goManagementPackageDirTestCases = generateManagementClientTestCases( +const goManagementPackageDirTestCases = createEmitterOptionTestCases( emitters.go, rulePath, "tspconfig-go-mgmt-package-dir-match-pattern", @@ -97,7 +106,7 @@ const goManagementPackageDirTestCases = generateManagementClientTestCases( "aaa", ); -const goManagementModuleTestCases = generateManagementClientTestCases( +const goManagementModuleTestCases = createEmitterOptionTestCases( emitters.go, rulePath, "tspconfig-go-mgmt-module-equal-string", @@ -107,7 +116,7 @@ const goManagementModuleTestCases = generateManagementClientTestCases( "github.com/Azure/azure-sdk-for-java/{service-dir}/{package-dir}", ); -const goManagementFixConstStutteringTestCases = generateManagementClientTestCases( +const goManagementFixConstStutteringTestCases = createEmitterOptionTestCases( emitters.go, rulePath, "tspconfig-go-mgmt-fix-const-stuttering-true", @@ -117,7 +126,7 @@ const goManagementFixConstStutteringTestCases = generateManagementClientTestCase false, ); -const goManagementGenerateExamplesTestCases = generateManagementClientTestCases( +const goManagementGenerateExamplesTestCases = createEmitterOptionTestCases( emitters.go, rulePath, "tspconfig-go-mgmt-generate-examples-true", @@ -127,7 +136,7 @@ const goManagementGenerateExamplesTestCases = generateManagementClientTestCases( false, ); -const goManagementGenerateFakesTestCases = generateManagementClientTestCases( +const goManagementGenerateFakesTestCases = createEmitterOptionTestCases( emitters.go, rulePath, "tspconfig-go-mgmt-generate-fakes-true", @@ -137,7 +146,7 @@ const goManagementGenerateFakesTestCases = generateManagementClientTestCases( false, ); -const goManagementHeadAsBooleanTestCases = generateManagementClientTestCases( +const goManagementHeadAsBooleanTestCases = createEmitterOptionTestCases( emitters.go, rulePath, "tspconfig-go-mgmt-head-as-boolean-true", @@ -147,7 +156,7 @@ const goManagementHeadAsBooleanTestCases = generateManagementClientTestCases( false, ); -const goManagementInjectSpansTestCases = generateManagementClientTestCases( +const goManagementInjectSpansTestCases = createEmitterOptionTestCases( emitters.go, rulePath, "tspconfig-go-mgmt-inject-spans-true", @@ -157,54 +166,100 @@ const goManagementInjectSpansTestCases = generateManagementClientTestCases( false, ); -function generateManagementClientTestCases( - emitterName: string, - rulePath: string, - ruleName: string, - fileName: string, - optionName: string, - validOptionValue: boolean | string, - invalidOptionValue: boolean | string, -): Case[] { - const managementGenerateMetadataTestCases: Case[] = [ - { - description: `valid: ${optionName} is ${validOptionValue}`, - rulePath, - ruleName, - fileName, - yamlContent: createEmitterOptions( - emitterName, - { key: optionName, value: validOptionValue }, - { key: "flavor", value: "azure" }, - ), - shouldReportError: false, - }, - { - description: `invalid: ${optionName} is ${invalidOptionValue}`, - rulePath, - ruleName, - fileName, - yamlContent: createEmitterOptions( - emitterName, - { key: optionName, value: invalidOptionValue }, - { key: "flavor", value: "azure" }, - ), - shouldReportError: true, - }, - { - description: `invalid: ${optionName} is undefined`, - rulePath, - ruleName, - fileName, - yamlContent: createEmitterOptions(emitterName, { key: "flavor", value: "azure" }), - shouldReportError: true, - }, - ]; - return managementGenerateMetadataTestCases; -} +const javaManagementPackageDirTestCases = createEmitterOptionTestCases( + emitters.java, + rulePath, + "tspconfig-java-mgmt-package-dir-match-pattern", + managementTspconfigPath, + "package-dir", + "azure-aaa", + "aaa", +); + +const pythonManagementPackageDirTestCases = createEmitterOptionTestCases( + emitters.python, + rulePath, + "tspconfig-python-mgmt-package-dir-match-pattern", + managementTspconfigPath, + "package-dir", + "azure-mgmt-aaa", + "azure-aaa", +); + +const pythonManagementPackageNameTestCases = createEmitterOptionTestCases( + emitters.python, + rulePath, + "tspconfig-python-mgmt-package-name-equal-string", + managementTspconfigPath, + "package-name", + "{package-dir}", + "aaa", +); + +const pythonManagementGenerateTestTestCases = createEmitterOptionTestCases( + emitters.python, + rulePath, + "tspconfig-python-mgmt-generate-test-true", + managementTspconfigPath, + "generate-test", + true, + false, +); + +const pythonManagementGenerateSampleTestCases = createEmitterOptionTestCases( + emitters.python, + rulePath, + "tspconfig-python-mgmt-generate-sample-true", + managementTspconfigPath, + "generate-sample", + true, + false, +); + +const csharpAzPackageDirTestCases = createEmitterOptionTestCases( + emitters.csharp, + rulePath, + "tspconfig-csharp-az-package-dir-match-pattern", + "", + "package-dir", + "Azure.AAA", + "AAA", +); + +const csharpAzNamespaceTestCases = createEmitterOptionTestCases( + emitters.csharp, + rulePath, + "tspconfig-csharp-az-namespace-equal-string", + "", + "namespace", + "{package-dir}", + "AAA", +); + +const csharpAzClearOutputFolderTestCases = createEmitterOptionTestCases( + emitters.csharp, + rulePath, + "tspconfig-csharp-az-clear-output-folder-true", + "", + "clear-output-folder", + true, + false, +); + +const csharpMgmtPackageDirTestCases = createEmitterOptionTestCases( + emitters.csharp, + rulePath, + "tspconfig-csharp-mgmt-package-dir-match-pattern", + managementTspconfigPath, + "package-dir", + "Azure.ResourceManager.AAA", + "Azure.Management.AAA", +); describe("Tspconfig emitter options validation", () => { it.each([ + // common + ...commonAzureServiceDirTestCases, // ts ...tsManagementGenerateMetadataTestCases, ...tsManagementHierarchyClientTestCases, @@ -221,6 +276,18 @@ describe("Tspconfig emitter options validation", () => { ...goManagementGenerateFakesTestCases, ...goManagementHeadAsBooleanTestCases, ...goManagementInjectSpansTestCases, + // java + ...javaManagementPackageDirTestCases, + // python + ...pythonManagementPackageDirTestCases, + ...pythonManagementPackageNameTestCases, + ...pythonManagementGenerateTestTestCases, + ...pythonManagementGenerateSampleTestCases, + // csharp + ...csharpAzPackageDirTestCases, + ...csharpAzNamespaceTestCases, + ...csharpAzClearOutputFolderTestCases, + ...csharpMgmtPackageDirTestCases, ])("$ruleName - $description", async (c: Case) => { const ruleTester = new RuleTester({ languageOptions: { @@ -253,3 +320,86 @@ describe("Tspconfig emitter options validation", () => { ruleTester.run(rule.name, rule as Rule.RuleModule, tests); }); }); + +function createEmitterOptionTestCases( + emitterName: string, + rulePath: string, + ruleName: string, + fileName: string, + key: string, + validValue: boolean | string, + invalidValue: boolean | string, +): Case[] { + const managementGenerateMetadataTestCases: Case[] = [ + { + description: `valid: ${key} is ${validValue}`, + rulePath, + ruleName, + fileName, + yamlContent: createEmitterOptionExample( + emitterName, + { key: key, value: validValue }, + { key: "flavor", value: "azure" }, + ), + shouldReportError: false, + }, + { + description: `invalid: ${key} is ${invalidValue}`, + rulePath, + ruleName, + fileName, + yamlContent: createEmitterOptionExample( + emitterName, + { key: key, value: invalidValue }, + { key: "flavor", value: "azure" }, + ), + shouldReportError: true, + }, + { + description: `invalid: ${key} is undefined`, + rulePath, + ruleName, + fileName, + yamlContent: createEmitterOptionExample(emitterName, { key: "flavor", value: "azure" }), + shouldReportError: true, + }, + ]; + return managementGenerateMetadataTestCases; +} + +function createParameterTestCases( + rulePath: string, + ruleName: string, + fileName: string, + key: string, + validValue: boolean | string, + invalidValue: boolean | string, +): Case[] { + const managementGenerateMetadataTestCases: Case[] = [ + { + description: `valid: ${key} is ${validValue}`, + rulePath, + ruleName, + fileName, + yamlContent: createParameterExample({ key: key, value: validValue }), + shouldReportError: false, + }, + { + description: `invalid: ${key} is ${invalidValue}`, + rulePath, + ruleName, + fileName, + yamlContent: createParameterExample({ key: key, value: invalidValue }), + shouldReportError: true, + }, + { + description: `invalid: ${key} is undefined`, + rulePath, + ruleName, + fileName, + yamlContent: "", + shouldReportError: true, + }, + ]; + return managementGenerateMetadataTestCases; +} From 3c767cfec373e503808ce845668508e44e8915e8 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Thu, 23 Jan 2025 10:42:41 +0800 Subject: [PATCH 66/93] update rule setup --- eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts | 6 ++++++ eng/tools/eslint-plugin-tsv/test/e2e.test.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts index 3e1867522c3f..027cee59ab7a 100644 --- a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts +++ b/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -2,6 +2,7 @@ import parser from "yaml-eslint-parser"; import { NamedESLint } from "./interfaces/named-eslint.js"; import emitAutorest from "./rules/emit-autorest.js"; import kebabCaseOrg from "./rules/kebab-case-org.js"; +import tspconfigValidationRules from "./rules/tspconfig-validation-rules.js"; const plugin: NamedESLint.Plugin = { configs: { recommended: {} }, @@ -26,4 +27,9 @@ plugin.configs.recommended = { }, }; +tspconfigValidationRules.forEach((rule) => { + plugin.rules![rule.name] = rule; + plugin.configs.recommended.rules![`${plugin.name}/${rule.name}`] = "error"; +}); + export default plugin; diff --git a/eng/tools/eslint-plugin-tsv/test/e2e.test.ts b/eng/tools/eslint-plugin-tsv/test/e2e.test.ts index 7c8ed1376584..e6da7e107a9d 100644 --- a/eng/tools/eslint-plugin-tsv/test/e2e.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/e2e.test.ts @@ -5,7 +5,7 @@ import eslintPluginTsv from "../src/eslint-plugin-tsv.js"; function createESLint() { return new ESLint({ - cwd: "/", + cwd: join(__dirname, "../../../../"), overrideConfig: eslintPluginTsv.configs.recommended, overrideConfigFile: true, }); From 68aa3950b16e6c2616d0e5f590e7033c059e0f88 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Thu, 23 Jan 2025 15:37:13 +0800 Subject: [PATCH 67/93] remove workflow --- .github/workflows/eslint-plugin-tsv-test.yaml | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 .github/workflows/eslint-plugin-tsv-test.yaml diff --git a/.github/workflows/eslint-plugin-tsv-test.yaml b/.github/workflows/eslint-plugin-tsv-test.yaml deleted file mode 100644 index 9fcdc7b47f46..000000000000 --- a/.github/workflows/eslint-plugin-tsv-test.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: ESLint Plugin for TypeSpec Validation - Test - -on: - push: - branches: - - main - - typespec-next - pull_request: - paths: - - package-lock.json - - package.json - - tsconfig.json - - .github/workflows/_reusable-eng-tools-test.yaml - - .github/workflows/eslint-plugin-tsv-test.yaml - - eng/tools/package.json - - eng/tools/tsconfig.json - - eng/tools/eslint-plugin-tsv/** - - specification/contosowidgetmanager - workflow_dispatch: - -jobs: - eslint-plugin-tsv: - uses: ./.github/workflows/_reusable-eng-tools-test.yaml - with: - package: eslint-plugin-tsv From cac050447a03320ef151eddaf3257db7f2b031f8 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Thu, 23 Jan 2025 15:39:29 +0800 Subject: [PATCH 68/93] remove an unused file --- .../src/utils/config-interpolation.ts | 221 ------------------ 1 file changed, 221 deletions(-) delete mode 100644 eng/tools/eslint-plugin-tsv/src/utils/config-interpolation.ts diff --git a/eng/tools/eslint-plugin-tsv/src/utils/config-interpolation.ts b/eng/tools/eslint-plugin-tsv/src/utils/config-interpolation.ts deleted file mode 100644 index cd7113383d03..000000000000 --- a/eng/tools/eslint-plugin-tsv/src/utils/config-interpolation.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { - ConfigParameter, - EmitterOptions, - ConfigEnvironmentVariable, - TypeSpecConfig, -} from "../config/types.js"; - -// dummy -interface Diagnostic {} -type DiagnosticResult = [T, readonly Diagnostic[]]; -const NoTarget = 0; -function createDiagnosticCollector() { - return { - pipe: (x: DiagnosticResult): T => { - return x[0]; - }, - wrap: (x: T): [T, [Diagnostic]] => { - return [x, [{}]]; - }, - }; -} -function ignoreDiagnostics(result: DiagnosticResult): T { - return result[0]; -} -function createDiagnostic(diag: any): Diagnostic { - return {}; -} -// - -// Copied from https://github.com/microsoft/typespec/blob/main/packages/compiler/src/config/config-interpolation.ts -export interface ExpandConfigOptions { - readonly cwd: string; - readonly outputDir?: string; - readonly env?: Record; - readonly args?: Record; -} - -export function expandConfigVariables( - config: TypeSpecConfig, - expandOptions: ExpandConfigOptions, -): [TypeSpecConfig, readonly Diagnostic[]] { - const diagnostics = createDiagnosticCollector(); - const builtInVars = { - "project-root": config.projectRoot, - cwd: expandOptions.cwd, - }; - - const resolvedArgsParameters = diagnostics.pipe( - resolveArgs(config.parameters, expandOptions.args, builtInVars), - ); - const commonVars = { - ...builtInVars, - ...resolvedArgsParameters, - ...diagnostics.pipe(resolveArgs(config.options, {}, resolvedArgsParameters)), - env: diagnostics.pipe( - resolveArgs(config.environmentVariables, expandOptions.env, builtInVars, true), - ), - }; - const outputDir = diagnostics.pipe( - resolveValue(expandOptions.outputDir ?? config.outputDir, commonVars), - ); - - const result = { ...config, outputDir }; - if (config.options) { - const options: Record = {}; - for (const [name, emitterOptions] of Object.entries(config.options)) { - const emitterVars = { ...commonVars, "output-dir": outputDir, "emitter-name": name }; - options[name] = diagnostics.pipe(resolveValues(emitterOptions, emitterVars)); - } - result.options = options; - } - - return diagnostics.wrap(result); -} - -function resolveArgs( - declarations: - | Record - | undefined, - args: Record | undefined, - predefinedVariables: Record>, - allowUnspecified = false, -): [Record, readonly Diagnostic[]] { - function tryGetValue(value: any): string | undefined { - return typeof value === "string" ? value : undefined; - } - const unmatchedArgs = new Set(Object.keys(args ?? {})); - const result: Record = {}; - - function resolveNestedArgs( - parentName: string, - declarations: [string, ConfigParameter | ConfigEnvironmentVariable | EmitterOptions][], - ) { - for (const [declarationName, definition] of declarations) { - const name = parentName ? `${parentName}.${declarationName}` : declarationName; - if (hasNestedValues(definition)) { - resolveNestedArgs(name, Object.entries(definition ?? {})); - } - unmatchedArgs.delete(name); - result[name] = ignoreDiagnostics( - resolveValue( - args?.[name] ?? tryGetValue(definition.default) ?? tryGetValue(definition) ?? "", - predefinedVariables, - ), - ); - } - } - - if (declarations !== undefined) { - resolveNestedArgs("", Object.entries(declarations ?? {})); - } - - if (!allowUnspecified) { - const diagnostics: Diagnostic[] = [...unmatchedArgs].map((unmatchedArg) => { - return createDiagnostic({ - code: "config-invalid-argument", - format: { name: unmatchedArg }, - target: NoTarget, - }); - }); - return [result, diagnostics]; - } - return [result, []]; -} - -function hasNestedValues(value: any): boolean { - return ( - value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0 - ); -} - -const VariableInterpolationRegex = /{([a-zA-Z-_.]+)}/g; - -function resolveValue( - value: string, - predefinedVariables: Record>, -): [string, readonly Diagnostic[]] { - const [result, diagnostics] = resolveValues({ value }, predefinedVariables); - return [result.value, diagnostics]; -} - -export function resolveValues>( - values: T, - predefinedVariables: Record> = {}, -): [T, readonly Diagnostic[]] { - const diagnostics: Diagnostic[] = []; - const resolvedValues: Record = {}; - const resolvingValues = new Set(); - - function resolveValue(keys: string[]): unknown { - resolvingValues.add(keys[0]); - let value: any = values; - value = keys.reduce((acc, key) => acc?.[key], value); - - if (typeof value !== "string") { - if (hasNestedValues(value)) { - value = value as Record; - const resultObject: Record = {}; - for (const [nestedKey] of Object.entries(value)) { - resolvingValues.add(nestedKey); - resultObject[nestedKey] = resolveValue(keys.concat(nestedKey)) as any; - } - return resultObject; - } - return value; - } - return value.replace(VariableInterpolationRegex, (match, expression) => { - return (resolveExpression(expression) as string) ?? `{${expression}}`; - }); - } - - function resolveExpression(expression: string): unknown | undefined { - if (expression in resolvedValues) { - return resolvedValues[expression]; - } - - if (resolvingValues.has(expression)) { - diagnostics.push( - createDiagnostic({ - code: "config-circular-variable", - target: NoTarget, - format: { name: expression }, - }), - ); - return undefined; - } - - if (expression in values) { - return resolveValue([expression]) as any; - } - - let resolved: any = predefinedVariables; - if (expression in resolved) { - return resolved[expression]; - } - - const segments = expression.split("."); - for (const segment of segments) { - resolved = resolved[segment]; - if (resolved === undefined) { - return undefined; - } - } - - if (typeof resolved === "string") { - return resolved; - } else { - return undefined; - } - } - - for (const key of Object.keys(values)) { - resolvingValues.clear(); - if (key in resolvedValues) { - continue; - } - resolvedValues[key] = resolveValue([key]) as any; - } - - return [resolvedValues as any, diagnostics]; -} From a0dbd75d45a90a3d99ac900824684bf02976ec42 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Thu, 23 Jan 2025 17:17:50 +0800 Subject: [PATCH 69/93] converting --- eng/tools/eslint-plugin-tsv/src/index.ts | 3 + eng/tools/typespec-validation/package.json | 1 + .../src/rules/tspconfig-validation-rules.ts | 35 + .../test/tspconfig.test.ts | 5 + package-lock.json | 995 ++++++++++++++++++ 5 files changed, 1039 insertions(+) create mode 100644 eng/tools/eslint-plugin-tsv/src/index.ts create mode 100644 eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts diff --git a/eng/tools/eslint-plugin-tsv/src/index.ts b/eng/tools/eslint-plugin-tsv/src/index.ts new file mode 100644 index 000000000000..24e0bf0e1925 --- /dev/null +++ b/eng/tools/eslint-plugin-tsv/src/index.ts @@ -0,0 +1,3 @@ +import tsvPlugin from "./eslint-plugin-tsv.js"; + +export default tsvPlugin; diff --git a/eng/tools/typespec-validation/package.json b/eng/tools/typespec-validation/package.json index c4cdbafaa805..c2516ce524f2 100644 --- a/eng/tools/typespec-validation/package.json +++ b/eng/tools/typespec-validation/package.json @@ -10,6 +10,7 @@ "globby": "^14.0.1", "simple-git": "^3.24.0", "suppressions": "file:../suppressions", + "eslint-plugin-tsv": "file:../eslint-plugin-tsv", "yaml": "^2.4.2" }, "devDependencies": { diff --git a/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts b/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts new file mode 100644 index 000000000000..61ebf2dad014 --- /dev/null +++ b/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts @@ -0,0 +1,35 @@ +import { join } from "path"; +import { parse as yamlParse } from "yaml"; +import { Rule } from "../rule.js"; +import { RuleResult } from "../rule-result.js"; +import { TsvHost } from "../tsv-host.js"; + +import tsvPlugin from "eslint-plugin-tsv" + +function convertToOldRules() { + console.log('plugin name', tsvPlugin); + + let newRules = []; + for (const [_, rule] of Object.entries(tsvPlugin.rules??{})) { + if (!rule.name.startsWith("tspconfig-")) continue; + const oldRule: Rule = { + name: rule.name, + description: rule.meta?.docs?.description ?? "", + async execute(host: TsvHost, folder: string): Promise { + const configText = await host.readTspConfig(folder); + const config = yamlParse(configText); + + const ruleListener = rule.create(context); + return { + }; + } + } + newRules.push(oldRule); + } + +} + +const rules = convertToOldRules(); + + +export default rules; \ No newline at end of file diff --git a/eng/tools/typespec-validation/test/tspconfig.test.ts b/eng/tools/typespec-validation/test/tspconfig.test.ts index 3be4e4f1deb3..6c09cbe1bfea 100644 --- a/eng/tools/typespec-validation/test/tspconfig.test.ts +++ b/eng/tools/typespec-validation/test/tspconfig.test.ts @@ -1,6 +1,7 @@ import { describe, it } from "vitest"; import { join } from "path"; import { TspConfigJavaPackageDirectoryRule } from "../src/rules/tspconfig-java-package-dir.js"; +import tspconfigRules from "../src/rules/tspconfig-validation-rules.js"; import { TsvTestHost } from "./tsv-test-host.js"; import { strict as assert, strictEqual } from "node:assert"; import { Rule } from "../src/rule.js"; @@ -98,3 +99,7 @@ describe("tspconfig", function () { }, ); }); + +describe("convert new ruls to old rules", function () { + +}) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f1ffe4f00aa3..e2e388ec99ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "dev": true, "hasInstallScript": true, "devDependencies": { + "@azure-tools/eslint-plugin-tsv": "file:eslint-plugin-tsv", "@azure-tools/sdk-suppressions": "file:sdk-suppressions", "@azure-tools/specs-model": "file:specs-model", "@azure-tools/suppressions": "file:suppressions", @@ -48,6 +49,798 @@ "@azure-tools/typespec-validation": "file:typespec-validation" } }, + "eng/tools/eslint-plugin-tsv": { + "dev": true, + "dependencies": { + "ajv": "^8.17.1", + "yaml-eslint-parser": "^1.2.3" + }, + "devDependencies": { + "@types/node": "^18.19.31", + "@vitest/coverage-v8": "^2.0.4", + "eslint": "^9.17.0", + "memfs": "^4.15.0", + "rimraf": "^5.0.10", + "typescript": "~5.6.2", + "vitest": "^2.0.4" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0" + } + }, + "eng/tools/eslint-plugin-tsv/node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "eng/tools/eslint-plugin-tsv/node_modules/@vitest/coverage-v8": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.8.tgz", + "integrity": "sha512-2Y7BPlKH18mAZYAW1tYByudlCYrQyl5RGvnnDYJKW5tCiO5qg3KSAy3XAxcxKz900a0ZXxWtKrMuZLe3lKBpJw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.7", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.12", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.8", + "vitest": "2.1.8" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "eng/tools/eslint-plugin-tsv/node_modules/@vitest/expect": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz", + "integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "eng/tools/eslint-plugin-tsv/node_modules/@vitest/pretty-format": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", + "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "eng/tools/eslint-plugin-tsv/node_modules/@vitest/runner": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz", + "integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==", + "dev": true, + "dependencies": { + "@vitest/utils": "2.1.8", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "eng/tools/eslint-plugin-tsv/node_modules/@vitest/snapshot": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz", + "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.8", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "eng/tools/eslint-plugin-tsv/node_modules/@vitest/spy": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz", + "integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==", + "dev": true, + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "eng/tools/eslint-plugin-tsv/node_modules/@vitest/utils": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", + "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.8", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "eng/tools/eslint-plugin-tsv/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "eng/tools/eslint-plugin-tsv/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "eng/tools/eslint-plugin-tsv/node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "eng/tools/eslint-plugin-tsv/node_modules/vite-node": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz", + "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "eng/tools/eslint-plugin-tsv/node_modules/vite-node/node_modules/vite": { + "version": "5.4.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", + "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "eng/tools/eslint-plugin-tsv/node_modules/vitest": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz", + "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==", + "dev": true, + "dependencies": { + "@vitest/expect": "2.1.8", + "@vitest/mocker": "2.1.8", + "@vitest/pretty-format": "^2.1.8", + "@vitest/runner": "2.1.8", + "@vitest/snapshot": "2.1.8", + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.8", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.8", + "@vitest/ui": "2.1.8", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "eng/tools/eslint-plugin-tsv/node_modules/vitest/node_modules/@vitest/mocker": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz", + "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "eng/tools/eslint-plugin-tsv/node_modules/vitest/node_modules/vite": { + "version": "5.4.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", + "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "eng/tools/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "eng/tools/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "eng/tools/node_modules/@types/node": { "version": "18.19.71", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.71.tgz", @@ -587,6 +1380,10 @@ "node": ">=12.0.0" } }, + "node_modules/@azure-tools/eslint-plugin-tsv": { + "resolved": "eng/tools/eslint-plugin-tsv", + "link": true + }, "node_modules/@azure-tools/openapi-tools-common": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@azure-tools/openapi-tools-common/-/openapi-tools-common-1.2.2.tgz", @@ -2360,6 +3157,60 @@ "jsep": "^0.4.0||^1.0.0" } }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.1.1.tgz", + "integrity": "sha512-osjeBqMJ2lb/j/M8NCPjs1ylqWIcTRTycIhVB5pt6LgzgeRSb0YRZ7j9RfA8wIUrsr/medIuhVyonXRZWLyfdw==", + "dev": true, + "dependencies": { + "@jsonjoy.com/base64": "^1.1.1", + "@jsonjoy.com/util": "^1.1.2", + "hyperdyperid": "^1.2.0", + "thingies": "^1.20.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", + "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -5992,6 +6843,15 @@ "dev": true, "license": "Unlicense" }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "engines": { + "node": ">=10.18" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -6821,6 +7681,25 @@ "dev": true, "license": "MIT" }, + "node_modules/memfs": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.0.tgz", + "integrity": "sha512-4eirfZ7thblFmqFjywlTmuWVSvccHAJbn1r8qQLzmTO11qcqpohOjmY2mFce6x7x7WtskzRqApPD0hv+Oa74jg==", + "dev": true, + "dependencies": { + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.3.0", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -8372,6 +9251,65 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.31.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.31.0.tgz", @@ -9047,6 +9985,18 @@ "dev": true, "license": "MIT" }, + "node_modules/thingies": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", + "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", + "dev": true, + "engines": { + "node": ">=10.18" + }, + "peerDependencies": { + "tslib": "^2" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -9128,6 +10078,22 @@ "dev": true, "license": "MIT" }, + "node_modules/tree-dump": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", + "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -9872,6 +10838,35 @@ "node": ">= 14" } }, + "node_modules/yaml-eslint-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/yaml-eslint-parser/-/yaml-eslint-parser-1.2.3.tgz", + "integrity": "sha512-4wZWvE398hCP7O8n3nXKu/vdq1HcH01ixYlCREaJL5NUMwQ0g3MaGFUBNSlmBtKmhbtVG/Cm6lyYmSVTEVil8A==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.0.0", + "lodash": "^4.17.21", + "yaml": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, + "node_modules/yaml-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", From e7dd065a160c31aa3e2341654fde2de9f8444b64 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 00:23:20 +0800 Subject: [PATCH 70/93] WIP --- eng/tools/eslint-plugin-tsv/src/index.ts | 3 + .../src/rules/tspconfig-validation-rules.ts | 46 +++++-- .../test/tspconfig.test.ts | 123 +++++++++--------- 3 files changed, 101 insertions(+), 71 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/src/index.ts b/eng/tools/eslint-plugin-tsv/src/index.ts index 24e0bf0e1925..1e0a6b8bfcfe 100644 --- a/eng/tools/eslint-plugin-tsv/src/index.ts +++ b/eng/tools/eslint-plugin-tsv/src/index.ts @@ -1,3 +1,6 @@ +import { Rule, ESLint, Linter } from "eslint"; import tsvPlugin from "./eslint-plugin-tsv.js"; +import { parseForESLint, parseYAML } from "yaml-eslint-parser"; +export { parseForESLint, parseYAML, Rule as ESRule, ESLint, Linter }; export default tsvPlugin; diff --git a/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts b/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts index 61ebf2dad014..5bfabe7ed285 100644 --- a/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts +++ b/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts @@ -4,32 +4,52 @@ import { Rule } from "../rule.js"; import { RuleResult } from "../rule-result.js"; import { TsvHost } from "../tsv-host.js"; -import tsvPlugin from "eslint-plugin-tsv" +import tsvPlugin, { ESLint, ESRule, parseForESLint } from "eslint-plugin-tsv"; + +async function runESLint(content: string) { + const eslint = new ESLint({ + cwd: join(__dirname, "../../../../"), + overrideConfig: tsvPlugin.configs.recommended, + overrideConfigFile: true, + }); + const results = await eslint.lintText(content, {filePath: 'tspconfig.yaml'}); + return results; +} function convertToOldRules() { - console.log('plugin name', tsvPlugin); - - let newRules = []; - for (const [_, rule] of Object.entries(tsvPlugin.rules??{})) { + let oldRules = []; + for (const [_, rule] of Object.entries(tsvPlugin.rules ?? {})) { if (!rule.name.startsWith("tspconfig-")) continue; const oldRule: Rule = { name: rule.name, description: rule.meta?.docs?.description ?? "", async execute(host: TsvHost, folder: string): Promise { const configText = await host.readTspConfig(folder); - const config = yamlParse(configText); - - const ruleListener = rule.create(context); + // const parsed = parseForESLint(configText, {location: true, }); + // const node = parsed as unknown as ESRule.Node; + // console.log('---node', node) + // const context = createFakeRuleContext(folder); + // const ruleListener = rule.create(context); + // const runTspConfigRule = ruleListener.YAMLDocument as (node: ESRule.Node) => void; + // if (runTspConfigRule) runTspConfigRule(node); + console.log('---configText', configText); + const results = await runESLint(configText); + console.log('---messages', results.map(r => r.messages)); return { + stdOutput: "", + success: true, }; - } - } - newRules.push(oldRule); + }, + }; + oldRules.push(oldRule); } - + return oldRules; } const rules = convertToOldRules(); +export default function () { + return convertToOldRules(); +} -export default rules; \ No newline at end of file +// export default rules; diff --git a/eng/tools/typespec-validation/test/tspconfig.test.ts b/eng/tools/typespec-validation/test/tspconfig.test.ts index 6c09cbe1bfea..89fb455910e5 100644 --- a/eng/tools/typespec-validation/test/tspconfig.test.ts +++ b/eng/tools/typespec-validation/test/tspconfig.test.ts @@ -15,65 +15,76 @@ interface TestCase { } const testCases: TestCase[] = [ +// { +// rule: new TspConfigJavaPackageDirectoryRule(), +// folder: TsvTestHost.folder, +// when: "package-dir \"azure-abc\" is valid", +// tspconfig: ` +// options: +// "@azure-tools/typespec-java": +// package-dir: azure-abc +// `, +// expectedResult: true, +// }, +// { +// rule: new TspConfigJavaPackageDirectoryRule(), +// folder: TsvTestHost.folder, +// when: "tspconfig.yaml is not a valid yaml", +// tspconfig: `aaa`, +// expectedResult: false, +// }, +// { +// rule: new TspConfigJavaPackageDirectoryRule(), +// folder: TsvTestHost.folder, +// when: "java emitter has no options", +// tspconfig: ` +// options: +// "@azure-tools/typespec-ts": +// package-dir: com.azure.test +// `, +// expectedResult: false, +// }, +// { +// rule: new TspConfigJavaPackageDirectoryRule(), +// folder: TsvTestHost.folder, +// when: "java emitter options have no package-dir", +// tspconfig: ` +// options: +// "@azure-tools/typespec-java": +// x: com.azure.test +// `, +// expectedResult: false, +// }, +// { +// rule: new TspConfigJavaPackageDirectoryRule(), +// folder: TsvTestHost.folder, +// when: "package-dir \"azure.test\" is invalid", +// tspconfig: ` +// options: +// "@azure-tools/typespec-java": +// package-dir: azure.test +// `, +// expectedResult: false, +// }, +// { +// rule: new TspConfigJavaPackageDirectoryRule(), +// folder: TsvTestHost.folder, +// when: "package-dir \"azure-\" is invalid", +// tspconfig: ` +// options: +// "@azure-tools/typespec-java": +// package-dir: azure- +// `, +// expectedResult: false, +// }, { - rule: new TspConfigJavaPackageDirectoryRule(), - folder: TsvTestHost.folder, - when: "package-dir \"azure-abc\" is valid", - tspconfig: ` -options: - "@azure-tools/typespec-java": - package-dir: azure-abc -`, - expectedResult: true, - }, - { - rule: new TspConfigJavaPackageDirectoryRule(), - folder: TsvTestHost.folder, - when: "tspconfig.yaml is not a valid yaml", - tspconfig: `aaa`, - expectedResult: false, - }, - { - rule: new TspConfigJavaPackageDirectoryRule(), - folder: TsvTestHost.folder, - when: "java emitter has no options", - tspconfig: ` -options: - "@azure-tools/typespec-ts": - package-dir: com.azure.test -`, - expectedResult: false, - }, - { - rule: new TspConfigJavaPackageDirectoryRule(), - folder: TsvTestHost.folder, - when: "java emitter options have no package-dir", - tspconfig: ` -options: - "@azure-tools/typespec-java": - x: com.azure.test -`, - expectedResult: false, - }, - { - rule: new TspConfigJavaPackageDirectoryRule(), - folder: TsvTestHost.folder, - when: "package-dir \"azure.test\" is invalid", - tspconfig: ` -options: - "@azure-tools/typespec-java": - package-dir: azure.test -`, - expectedResult: false, - }, - { - rule: new TspConfigJavaPackageDirectoryRule(), + rule: tspconfigRules()[0], folder: TsvTestHost.folder, when: "package-dir \"azure-\" is invalid", tspconfig: ` options: "@azure-tools/typespec-java": - package-dir: azure- + package-dir: azure-aaa `, expectedResult: false, }, @@ -98,8 +109,4 @@ describe("tspconfig", function () { } }, ); -}); - -describe("convert new ruls to old rules", function () { - -}) \ No newline at end of file +}); \ No newline at end of file From 91c2f288f6d5c830a0b08c435aa5af9f4eb083b9 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 00:52:46 +0800 Subject: [PATCH 71/93] fix doc --- eng/tools/eslint-plugin-tsv/src/utils/constants.ts | 1 + eng/tools/eslint-plugin-tsv/src/utils/rule-doc.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/eng/tools/eslint-plugin-tsv/src/utils/constants.ts b/eng/tools/eslint-plugin-tsv/src/utils/constants.ts index 2ca356ba4f48..aff4758de554 100644 --- a/eng/tools/eslint-plugin-tsv/src/utils/constants.ts +++ b/eng/tools/eslint-plugin-tsv/src/utils/constants.ts @@ -5,6 +5,7 @@ export const emitters = { python: "@azure-tools/typespec-python", go: "@azure-tools/typespec-go", autorest: "@azure-tools/typespec-autorest", + common: "", }; export const defaultMessageId = "problem"; diff --git a/eng/tools/eslint-plugin-tsv/src/utils/rule-doc.ts b/eng/tools/eslint-plugin-tsv/src/utils/rule-doc.ts index f1525db38605..4a62792f0c11 100644 --- a/eng/tools/eslint-plugin-tsv/src/utils/rule-doc.ts +++ b/eng/tools/eslint-plugin-tsv/src/utils/rule-doc.ts @@ -57,7 +57,7 @@ export function createRuleDocument( let example = ""; switch (keyType) { case KeyType.EmitterOption: - displayName = `options.${emitters.ts}.${key}`; + displayName = `options.${emitterName}.${key}`; example = createEmitterOptionExample(emitterName, { key: key, value: exampleValue }); break; case KeyType.Parameter: From 06003ea3f22a6dfee2d3837c8f8736f2077e3673 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 00:57:46 +0800 Subject: [PATCH 72/93] WIP --- eng/tools/typespec-validation/test/tspconfig.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/tools/typespec-validation/test/tspconfig.test.ts b/eng/tools/typespec-validation/test/tspconfig.test.ts index 89fb455910e5..7e4983f13b47 100644 --- a/eng/tools/typespec-validation/test/tspconfig.test.ts +++ b/eng/tools/typespec-validation/test/tspconfig.test.ts @@ -90,7 +90,7 @@ options: }, ]; -describe("tspconfig", function () { +describe("tspconfig-xxx", function () { it.each(testCases)( `should be $expectedResult for rule $rule.name when $when`, async (c: TestCase) => { From 81508d0c6b1a79334dbfba3891c1f9cd5baae0ad Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 00:59:40 +0800 Subject: [PATCH 73/93] fix --- .../src/rules/tspconfig-validation-rules.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts index 719e370ac560..97a1fb10eea7 100644 --- a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts +++ b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts @@ -1,10 +1,6 @@ import { Rule } from "eslint"; import { TypeSpecConfig } from "../config/types.js"; -import { - createCodeGenSDKRule, - isAzureSDK, - isManagementSDK, -} from "../utils/rule-creator.js"; +import { createCodeGenSDKRule, isAzureSDK, isManagementSDK } from "../utils/rule-creator.js"; import { emitters } from "../utils/constants.js"; import { CreateCodeGenSDKRuleArgs, KeyType } from "../interfaces/rule-interfaces.js"; @@ -90,7 +86,8 @@ const args: CreateCodeGenSDKRuleArgs[] = [ exampleValue: "sdk/resourcemanager/aaa", extraExplanation: "The 'service-dir' should be a string that starts with 'sdk/resourcemanager/', followed by zero or more characters that are not a '/', and ends there", - condition: (_: TypeSpecConfig, _1: Rule.RuleContext) => true, + condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => + isManagementSDK(tspconfig, context, emitters.go), }, { rule: "tspconfig-go-mgmt-package-dir-match-pattern", From 73a85623aeb5a38701b83d8c89d36b4c9aa548bd Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 10:03:48 +0800 Subject: [PATCH 74/93] add simple test --- eng/tools/eslint-plugin-tsv/src/index.ts | 5 +- .../src/rules/tspconfig-validation-rules.ts | 44 ++--- .../test/tspconfig.test.ts | 165 ++++++++++-------- 3 files changed, 119 insertions(+), 95 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/src/index.ts b/eng/tools/eslint-plugin-tsv/src/index.ts index 1e0a6b8bfcfe..6e6e166c1421 100644 --- a/eng/tools/eslint-plugin-tsv/src/index.ts +++ b/eng/tools/eslint-plugin-tsv/src/index.ts @@ -1,6 +1,5 @@ -import { Rule, ESLint, Linter } from "eslint"; +import { ESLint } from "eslint"; import tsvPlugin from "./eslint-plugin-tsv.js"; -import { parseForESLint, parseYAML } from "yaml-eslint-parser"; -export { parseForESLint, parseYAML, Rule as ESRule, ESLint, Linter }; +export { ESLint }; export default tsvPlugin; diff --git a/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts b/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts index 5bfabe7ed285..f5628dac4f3e 100644 --- a/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts +++ b/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts @@ -1,21 +1,28 @@ +// Note: temporary workaround to convert new rules to old rules to provides suggestion to correct tspconfig + import { join } from "path"; -import { parse as yamlParse } from "yaml"; import { Rule } from "../rule.js"; import { RuleResult } from "../rule-result.js"; import { TsvHost } from "../tsv-host.js"; -import tsvPlugin, { ESLint, ESRule, parseForESLint } from "eslint-plugin-tsv"; +import tsvPlugin, { ESLint } from "eslint-plugin-tsv"; -async function runESLint(content: string) { +async function runESLint(content: string, folder: string, ruleName: string) { + const config = tsvPlugin.configs.recommended; + for (const key in config.rules) { + if (key !== "tsv/" + ruleName) delete config.rules[key]; + } const eslint = new ESLint({ cwd: join(__dirname, "../../../../"), overrideConfig: tsvPlugin.configs.recommended, overrideConfigFile: true, }); - const results = await eslint.lintText(content, {filePath: 'tspconfig.yaml'}); - return results; + const results = await eslint.lintText(content, { filePath: join(folder, "tspconfig.yaml") }); + return results; } +// NOTE: This is a workaround to convert the new rules to old rules +// To be removed when the new TSV framework is ready function convertToOldRules() { let oldRules = []; for (const [_, rule] of Object.entries(tsvPlugin.rules ?? {})) { @@ -25,18 +32,17 @@ function convertToOldRules() { description: rule.meta?.docs?.description ?? "", async execute(host: TsvHost, folder: string): Promise { const configText = await host.readTspConfig(folder); - // const parsed = parseForESLint(configText, {location: true, }); - // const node = parsed as unknown as ESRule.Node; - // console.log('---node', node) - // const context = createFakeRuleContext(folder); - // const ruleListener = rule.create(context); - // const runTspConfigRule = ruleListener.YAMLDocument as (node: ESRule.Node) => void; - // if (runTspConfigRule) runTspConfigRule(node); - console.log('---configText', configText); - const results = await runESLint(configText); - console.log('---messages', results.map(r => r.messages)); + const results = await runESLint(configText, folder, rule.name); + if (results.length > 0 && results[0].messages.length > 0) { + return { + errorOutput: results[0].messages[0].message, + // Only used to provide suggestion to correct tspconfig + success: true, + }; + } + return { - stdOutput: "", + stdOutput: `[${rule.name}]: validation passed.`, success: true, }; }, @@ -48,8 +54,4 @@ function convertToOldRules() { const rules = convertToOldRules(); -export default function () { - return convertToOldRules(); -} - -// export default rules; +export default rules; diff --git a/eng/tools/typespec-validation/test/tspconfig.test.ts b/eng/tools/typespec-validation/test/tspconfig.test.ts index 7e4983f13b47..ffb53cd0958c 100644 --- a/eng/tools/typespec-validation/test/tspconfig.test.ts +++ b/eng/tools/typespec-validation/test/tspconfig.test.ts @@ -15,82 +15,60 @@ interface TestCase { } const testCases: TestCase[] = [ -// { -// rule: new TspConfigJavaPackageDirectoryRule(), -// folder: TsvTestHost.folder, -// when: "package-dir \"azure-abc\" is valid", -// tspconfig: ` -// options: -// "@azure-tools/typespec-java": -// package-dir: azure-abc -// `, -// expectedResult: true, -// }, -// { -// rule: new TspConfigJavaPackageDirectoryRule(), -// folder: TsvTestHost.folder, -// when: "tspconfig.yaml is not a valid yaml", -// tspconfig: `aaa`, -// expectedResult: false, -// }, -// { -// rule: new TspConfigJavaPackageDirectoryRule(), -// folder: TsvTestHost.folder, -// when: "java emitter has no options", -// tspconfig: ` -// options: -// "@azure-tools/typespec-ts": -// package-dir: com.azure.test -// `, -// expectedResult: false, -// }, -// { -// rule: new TspConfigJavaPackageDirectoryRule(), -// folder: TsvTestHost.folder, -// when: "java emitter options have no package-dir", -// tspconfig: ` -// options: -// "@azure-tools/typespec-java": -// x: com.azure.test -// `, -// expectedResult: false, -// }, -// { -// rule: new TspConfigJavaPackageDirectoryRule(), -// folder: TsvTestHost.folder, -// when: "package-dir \"azure.test\" is invalid", -// tspconfig: ` -// options: -// "@azure-tools/typespec-java": -// package-dir: azure.test -// `, -// expectedResult: false, -// }, -// { -// rule: new TspConfigJavaPackageDirectoryRule(), -// folder: TsvTestHost.folder, -// when: "package-dir \"azure-\" is invalid", -// tspconfig: ` -// options: -// "@azure-tools/typespec-java": -// package-dir: azure- -// `, -// expectedResult: false, -// }, { - rule: tspconfigRules()[0], + rule: new TspConfigJavaPackageDirectoryRule(), folder: TsvTestHost.folder, - when: "package-dir \"azure-\" is invalid", + when: 'package-dir "azure-abc" is valid', tspconfig: ` -options: - "@azure-tools/typespec-java": - package-dir: azure-aaa -`, + options: + "@azure-tools/typespec-java": + package-dir: azure-abc + `, + expectedResult: true, + }, + { + rule: new TspConfigJavaPackageDirectoryRule(), + folder: TsvTestHost.folder, + when: "tspconfig.yaml is not a valid yaml", + tspconfig: `aaa`, + expectedResult: false, + }, + { + rule: new TspConfigJavaPackageDirectoryRule(), + folder: TsvTestHost.folder, + when: "java emitter has no options", + tspconfig: ` + options: + "@azure-tools/typespec-ts": + package-dir: com.azure.test + `, + expectedResult: false, + }, + { + rule: new TspConfigJavaPackageDirectoryRule(), + folder: TsvTestHost.folder, + when: "java emitter options have no package-dir", + tspconfig: ` + options: + "@azure-tools/typespec-java": + x: com.azure.test + `, + expectedResult: false, + }, + { + rule: new TspConfigJavaPackageDirectoryRule(), + folder: TsvTestHost.folder, + when: 'package-dir "azure.test" is invalid', + tspconfig: ` + options: + "@azure-tools/typespec-java": + package-dir: azure.test + `, expectedResult: false, }, ]; -describe("tspconfig-xxx", function () { +describe("tspconfig", function () { it.each(testCases)( `should be $expectedResult for rule $rule.name when $when`, async (c: TestCase) => { @@ -99,7 +77,7 @@ describe("tspconfig-xxx", function () { return file === join(TsvTestHost.folder, "tspconfig.yaml"); }; host.readTspConfig = async (_folder: string) => c.tspconfig; - const result = await c.rule.execute(host, TsvTestHost.folder); + const result = await c.rule.execute(host, c.folder); strictEqual(result.success, c.expectedResult); if (!c.expectedResult) { // TODO: assert link when ready @@ -109,4 +87,49 @@ describe("tspconfig-xxx", function () { } }, ); -}); \ No newline at end of file + + it.each([ + { + rule: tspconfigRules.find((r) => r.name === "tspconfig-java-mgmt-package-dir-match-pattern")!, + folder: "aaa/aaa.Management/", + when: 'package-dir "azure-" is invalid', + tspconfig: ` + options: + "@azure-tools/typespec-java": + package-dir: xxxxx + flavor: azure + `, + expectedResult: false, + }, + { + rule: tspconfigRules.find((r) => r.name === "tspconfig-java-mgmt-package-dir-match-pattern")!, + folder: "aaa/aaa.Management/", + when: 'package-dir "azure-" is invalid', + tspconfig: ` + options: + "@azure-tools/typespec-java": + package-dir: azure-test + flavor: azure + `, + expectedResult: true, + }, + ])(`should be $expectedResult for new rule $rule.name when $when`, async (c: TestCase) => { + let host = new TsvTestHost(); + host.checkFileExists = async (file: string) => { + return file === join(TsvTestHost.folder, "tspconfig.yaml"); + }; + host.readTspConfig = async (_folder: string) => c.tspconfig; + const result = await c.rule.execute(host, c.folder); + strictEqual(result.success, true); + assert( + (c.expectedResult && + result.stdOutput && + result.stdOutput.length > 0 && + result.errorOutput === undefined) || + (!c.expectedResult && + result.stdOutput === undefined && + result.errorOutput && + result.errorOutput.length > 0), + ); + }); +}); From f84257bed3d878f169bcf6787a79a606d53e0ff3 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 10:07:30 +0800 Subject: [PATCH 75/93] update java rule --- .../eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts | 4 ++-- .../test/rules/tspconfig-options-validation.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts index 97a1fb10eea7..55b79b10b597 100644 --- a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts +++ b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts @@ -156,7 +156,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ }, // java { - rule: "tspconfig-java-mgmt-package-dir-match-pattern", + rule: "tspconfig-java-az-package-dir-match-pattern", key: "package-dir", type: KeyType.EmitterOption, expectedValue: /^azure(-\w+)+$/, @@ -164,7 +164,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ extraExplanation: "The 'package-dir' should be a string that starts with 'azure', followed by one or more '-' segments. Each segment can contains letters, digits, or underscores", condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => - isManagementSDK(tspconfig, context, emitters.java), + isAzureSDK(tspconfig, emitters.java), }, // python { diff --git a/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts b/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts index a2456cefe83f..aecc83dc8b96 100644 --- a/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts @@ -169,8 +169,8 @@ const goManagementInjectSpansTestCases = createEmitterOptionTestCases( const javaManagementPackageDirTestCases = createEmitterOptionTestCases( emitters.java, rulePath, - "tspconfig-java-mgmt-package-dir-match-pattern", - managementTspconfigPath, + "tspconfig-java-az-package-dir-match-pattern", + "", "package-dir", "azure-aaa", "aaa", From 4d2c8f3aa2aec50b492f0a21c40efdca4dffe4fa Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 10:15:18 +0800 Subject: [PATCH 76/93] update test --- eng/tools/typespec-validation/test/tspconfig.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eng/tools/typespec-validation/test/tspconfig.test.ts b/eng/tools/typespec-validation/test/tspconfig.test.ts index ffb53cd0958c..2104d9811db6 100644 --- a/eng/tools/typespec-validation/test/tspconfig.test.ts +++ b/eng/tools/typespec-validation/test/tspconfig.test.ts @@ -90,7 +90,7 @@ describe("tspconfig", function () { it.each([ { - rule: tspconfigRules.find((r) => r.name === "tspconfig-java-mgmt-package-dir-match-pattern")!, + rule: tspconfigRules.find((r) => r.name === "tspconfig-java-az-package-dir-match-pattern")!, folder: "aaa/aaa.Management/", when: 'package-dir "azure-" is invalid', tspconfig: ` @@ -102,7 +102,7 @@ describe("tspconfig", function () { expectedResult: false, }, { - rule: tspconfigRules.find((r) => r.name === "tspconfig-java-mgmt-package-dir-match-pattern")!, + rule: tspconfigRules.find((r) => r.name === "tspconfig-java-az-package-dir-match-pattern")!, folder: "aaa/aaa.Management/", when: 'package-dir "azure-" is invalid', tspconfig: ` From db989a653a3679321fc2859bddf0917f56032037 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 10:48:34 +0800 Subject: [PATCH 77/93] add new rules --- eng/tools/typespec-validation/src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/eng/tools/typespec-validation/src/index.ts b/eng/tools/typespec-validation/src/index.ts index b72ea7616531..98192b48420b 100755 --- a/eng/tools/typespec-validation/src/index.ts +++ b/eng/tools/typespec-validation/src/index.ts @@ -8,6 +8,8 @@ import { LinterRulesetRule } from "./rules/linter-ruleset.js"; import { NpmPrefixRule } from "./rules/npm-prefix.js"; import { TsvRunnerHost } from "./tsv-runner-host.js"; import { getSuppressions, Suppression } from "suppressions"; +import tspconfigRules from "./rules/tspconfig-validation-rules.js"; +import { Rule } from "./rule.js"; export async function main() { const host = new TsvRunnerHost(); @@ -39,7 +41,7 @@ export async function main() { return; } - const rules = [ + let rules: Rule[] = [ new FolderStructureRule(), new NpmPrefixRule(), new EmitAutorestRule(), @@ -48,6 +50,8 @@ export async function main() { new CompileRule(), new FormatRule(), ]; + rules.push(...tspconfigRules); + let success = true; for (let i = 0; i < rules.length; i++) { const rule = rules[i]; From 2ba3d591d1899c84402dc35222d39ed6a3aacb13 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 10:51:33 +0800 Subject: [PATCH 78/93] remove files --- .../src/rules/tspconfig-java-package-dir.ts | 54 ------------- .../test/tspconfig.test.ts | 78 +------------------ 2 files changed, 2 insertions(+), 130 deletions(-) delete mode 100644 eng/tools/typespec-validation/src/rules/tspconfig-java-package-dir.ts diff --git a/eng/tools/typespec-validation/src/rules/tspconfig-java-package-dir.ts b/eng/tools/typespec-validation/src/rules/tspconfig-java-package-dir.ts deleted file mode 100644 index 28c8d544f13b..000000000000 --- a/eng/tools/typespec-validation/src/rules/tspconfig-java-package-dir.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { join } from "path"; -import { parse as yamlParse } from "yaml"; -import { Rule } from "../rule.js"; -import { RuleResult } from "../rule-result.js"; -import { TsvHost } from "../tsv-host.js"; - -export class TspConfigJavaPackageDirectoryRule implements Rule { - pattern = new RegExp(/^azure(-\w+)+$/); - - readonly name = "tspconfig-java-package-dir"; - readonly description = `"options.@azure-tools/typespec-java.package-dir" must match ${this.pattern}.`; - readonly action = `Please update "options.@azure-tools/typespec-java.package-dir" to start with "azure", followed by one or more "-" segments. Each segment can contains letters, digits, or underscores. For example: "azure-test".`; - // TODO: provide link to the rule details and full sample - readonly link = ""; - async execute(host: TsvHost, folder: string): Promise { - const tspconfigExists = await host.checkFileExists(join(folder, "tspconfig.yaml")); - if (!tspconfigExists) - return this.createFailedResult(`Failed to find ${join(folder, "tspconfig.yaml")}`); - - let config = undefined; - try { - const configText = await host.readTspConfig(folder); - config = yamlParse(configText); - } catch (error) { - // TODO: append content " Check tpsconfig-file-exists rule for more details." when it's ready - return this.createFailedResult(`Failed to parse ${join(folder, "tspconfig.yaml")}`); - } - - const javaEmitterOptions = config?.options?.["@azure-tools/typespec-java"]; - - if (!javaEmitterOptions) - return this.createFailedResult(`Failed to find "options.@azure-tools/typespec-java"`); - - const packageDir = javaEmitterOptions?.["package-dir"]; - if (!packageDir) - return this.createFailedResult( - `Failed to find "options.@azure-tools/typespec-java.package-dir"`, - ); - - if (!this.pattern.test(packageDir)) { - return this.createFailedResult( - `package-dir "${packageDir}" does not match "${this.pattern}"`, - ); - } - return { success: true, stdOutput: `[${this.name}]: validation passed.` }; - } - - createFailedResult(errorMessage: string): RuleResult { - return { - success: false, - errorOutput: `[${this.name}]: ${errorMessage}. ${this.description} ${this.action} For more information and full samples, see ${this.link}.`, - }; - } -} diff --git a/eng/tools/typespec-validation/test/tspconfig.test.ts b/eng/tools/typespec-validation/test/tspconfig.test.ts index 2104d9811db6..e7716d587c9a 100644 --- a/eng/tools/typespec-validation/test/tspconfig.test.ts +++ b/eng/tools/typespec-validation/test/tspconfig.test.ts @@ -1,6 +1,5 @@ import { describe, it } from "vitest"; import { join } from "path"; -import { TspConfigJavaPackageDirectoryRule } from "../src/rules/tspconfig-java-package-dir.js"; import tspconfigRules from "../src/rules/tspconfig-validation-rules.js"; import { TsvTestHost } from "./tsv-test-host.js"; import { strict as assert, strictEqual } from "node:assert"; @@ -14,84 +13,11 @@ interface TestCase { folder: string; } -const testCases: TestCase[] = [ - { - rule: new TspConfigJavaPackageDirectoryRule(), - folder: TsvTestHost.folder, - when: 'package-dir "azure-abc" is valid', - tspconfig: ` - options: - "@azure-tools/typespec-java": - package-dir: azure-abc - `, - expectedResult: true, - }, - { - rule: new TspConfigJavaPackageDirectoryRule(), - folder: TsvTestHost.folder, - when: "tspconfig.yaml is not a valid yaml", - tspconfig: `aaa`, - expectedResult: false, - }, - { - rule: new TspConfigJavaPackageDirectoryRule(), - folder: TsvTestHost.folder, - when: "java emitter has no options", - tspconfig: ` - options: - "@azure-tools/typespec-ts": - package-dir: com.azure.test - `, - expectedResult: false, - }, - { - rule: new TspConfigJavaPackageDirectoryRule(), - folder: TsvTestHost.folder, - when: "java emitter options have no package-dir", - tspconfig: ` - options: - "@azure-tools/typespec-java": - x: com.azure.test - `, - expectedResult: false, - }, - { - rule: new TspConfigJavaPackageDirectoryRule(), - folder: TsvTestHost.folder, - when: 'package-dir "azure.test" is invalid', - tspconfig: ` - options: - "@azure-tools/typespec-java": - package-dir: azure.test - `, - expectedResult: false, - }, -]; - -describe("tspconfig", function () { - it.each(testCases)( - `should be $expectedResult for rule $rule.name when $when`, - async (c: TestCase) => { - let host = new TsvTestHost(); - host.checkFileExists = async (file: string) => { - return file === join(TsvTestHost.folder, "tspconfig.yaml"); - }; - host.readTspConfig = async (_folder: string) => c.tspconfig; - const result = await c.rule.execute(host, c.folder); - strictEqual(result.success, c.expectedResult); - if (!c.expectedResult) { - // TODO: assert link when ready - assert(result.errorOutput?.includes(c.rule.name)); - assert(result.errorOutput?.includes(c.rule.description)); - assert(result.errorOutput?.includes(c.rule.action!)); - } - }, - ); - +describe("tspconfig rules", () => { it.each([ { rule: tspconfigRules.find((r) => r.name === "tspconfig-java-az-package-dir-match-pattern")!, - folder: "aaa/aaa.Management/", + folder: "aaa/bbb/", when: 'package-dir "azure-" is invalid', tspconfig: ` options: From 317a827afb717047b252e4778dbd6d6d579a3c4d Mon Sep 17 00:00:00 2001 From: v-tianxi Date: Fri, 24 Jan 2025 11:02:51 +0800 Subject: [PATCH 79/93] remove net-track2 --- specificationRepositoryConfiguration.json | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/specificationRepositoryConfiguration.json b/specificationRepositoryConfiguration.json index 9c6ad529e741..0b2c1930d549 100644 --- a/specificationRepositoryConfiguration.json +++ b/specificationRepositoryConfiguration.json @@ -17,10 +17,6 @@ "mainRepository": "Azure/azure-sdk-for-js" }, "azure-sdk-for-net": { - "integrationRepository": "azure-sdk/azure-sdk-for-net", - "mainRepository": "Azure/azure-sdk-for-net" - }, - "azure-sdk-for-net-track2": { "integrationRepository": "azure-sdk/azure-sdk-for-net", "mainRepository": "Azure/azure-sdk-for-net", "configFilePath": "eng/swagger_to_sdk_config.json" @@ -52,10 +48,6 @@ "mainRepository": "Azure/azure-sdk-for-js-pr" }, "azure-sdk-for-net": { - "integrationRepository": "azure-sdk/azure-sdk-for-net-pr", - "mainRepository": "Azure/azure-sdk-for-net-pr" - }, - "azure-sdk-for-net-track2": { "integrationRepository": "azure-sdk/azure-sdk-for-net-pr", "mainRepository": "Azure/azure-sdk-for-net-pr", "configFilePath": "eng/swagger_to_sdk_config.json" @@ -74,7 +66,7 @@ "typespecEmitterToSdkRepositoryMapping": { "@azure-tools/typespec-python": "azure-sdk-for-python", "@azure-tools/typespec-java": "azure-sdk-for-java", - "@azure-tools/typespec-csharp": "azure-sdk-for-net-track2", + "@azure-tools/typespec-csharp": "azure-sdk-for-net", "@azure-tools/typespec-ts": "azure-sdk-for-js", "@azure-tools/typespec-go": "azure-sdk-for-go" } From 9f84497737d8b19bc603f33ca62e25a00fa25755 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 11:02:55 +0800 Subject: [PATCH 80/93] fix lint error --- .../eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts | 2 +- eng/tools/eslint-plugin-tsv/src/utils/rule-doc.ts | 1 - package-lock.json | 2 ++ 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts index 55b79b10b597..f4c2610ff76d 100644 --- a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts +++ b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts @@ -163,7 +163,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ exampleValue: "azure-aaa", extraExplanation: "The 'package-dir' should be a string that starts with 'azure', followed by one or more '-' segments. Each segment can contains letters, digits, or underscores", - condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => + condition: (tspconfig: TypeSpecConfig, _: Rule.RuleContext) => isAzureSDK(tspconfig, emitters.java), }, // python diff --git a/eng/tools/eslint-plugin-tsv/src/utils/rule-doc.ts b/eng/tools/eslint-plugin-tsv/src/utils/rule-doc.ts index 4a62792f0c11..1ffef72bee77 100644 --- a/eng/tools/eslint-plugin-tsv/src/utils/rule-doc.ts +++ b/eng/tools/eslint-plugin-tsv/src/utils/rule-doc.ts @@ -1,5 +1,4 @@ import { KeyType, RuleDocument } from "../interfaces/rule-interfaces.js"; -import { emitters } from "./constants.js"; import { stringify } from "yaml"; function createDescriptionDocumentBlock( diff --git a/package-lock.json b/package-lock.json index 821663bc1e52..6791bde87d86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,10 +52,12 @@ "name": "@azure-tools/eslint-plugin-tsv", "dev": true, "dependencies": { + "ajv": "^8.17.1", "yaml-eslint-parser": "^1.2.3" }, "devDependencies": { "@types/node": "^18.19.31", + "@vitest/coverage-v8": "^2.0.4", "eslint": "^9.17.0", "memfs": "^4.15.0", "rimraf": "^5.0.10", From 4d1359c0ebcbfa70d406e8531ad85a06e70cd4b2 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 11:05:48 +0800 Subject: [PATCH 81/93] update lock file --- package-lock.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/package-lock.json b/package-lock.json index e2e388ec99ce..7037acc8b484 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ } }, "eng/tools/eslint-plugin-tsv": { + "name": "@azure-tools/eslint-plugin-tsv", "dev": true, "dependencies": { "ajv": "^8.17.1", @@ -1179,6 +1180,7 @@ "name": "@azure-tools/typespec-validation", "dev": true, "dependencies": { + "eslint-plugin-tsv": "file:../eslint-plugin-tsv", "globby": "^14.0.1", "simple-git": "^3.24.0", "suppressions": "file:../suppressions", @@ -5690,6 +5692,10 @@ } } }, + "node_modules/eslint-plugin-tsv": { + "resolved": "eng/tools/eslint-plugin-tsv", + "link": true + }, "node_modules/eslint-plugin-unicorn": { "version": "56.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.1.tgz", From 4e7e4eec74f8eca998cbf49b5082c2934cc3fe1e Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 15:29:29 +0800 Subject: [PATCH 82/93] delete flavor rule --- .../src/rules/tspconfig-validation-rules.ts | 55 +++++------- .../src/utils/rule-creator.ts | 31 ++++--- .../tspconfig-options-validation.test.ts | 14 +--- eng/tools/typespec-validation/src/index.ts | 2 - .../src/rules/flavor-azure.ts | 53 ------------ .../test/flavor-azure.test.ts | 84 ------------------- 6 files changed, 37 insertions(+), 202 deletions(-) delete mode 100644 eng/tools/typespec-validation/src/rules/flavor-azure.ts delete mode 100644 eng/tools/typespec-validation/test/flavor-azure.test.ts diff --git a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts index f4c2610ff76d..83f193b825f7 100644 --- a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts +++ b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts @@ -1,6 +1,6 @@ import { Rule } from "eslint"; import { TypeSpecConfig } from "../config/types.js"; -import { createCodeGenSDKRule, isAzureSDK, isManagementSDK } from "../utils/rule-creator.js"; +import { createCodeGenSDKRule, isManagementSDK } from "../utils/rule-creator.js"; import { emitters } from "../utils/constants.js"; import { CreateCodeGenSDKRuleArgs, KeyType } from "../interfaces/rule-interfaces.js"; @@ -9,7 +9,7 @@ const tsIsManagementCondition = (tspconfig: TypeSpecConfig, context: Rule.RuleCo const isModularLibrary = tspconfig.options?.[emitterName]?.isModularLibrary as | boolean | undefined; - return isManagementSDK(tspconfig, context, emitterName) && isModularLibrary !== false; + return isManagementSDK(context) && isModularLibrary !== false; }; const args: CreateCodeGenSDKRuleArgs[] = [ @@ -86,8 +86,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ exampleValue: "sdk/resourcemanager/aaa", extraExplanation: "The 'service-dir' should be a string that starts with 'sdk/resourcemanager/', followed by zero or more characters that are not a '/', and ends there", - condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => - isManagementSDK(tspconfig, context, emitters.go), + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), }, { rule: "tspconfig-go-mgmt-package-dir-match-pattern", @@ -97,8 +96,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ exampleValue: "armaaa", extraExplanation: "The 'package-dir' should be a string that starts with 'arm' and do not contain a forward slash (/) after it", - condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => - isManagementSDK(tspconfig, context, emitters.go), + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), }, { rule: "tspconfig-go-mgmt-module-equal-string", @@ -106,8 +104,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ type: KeyType.EmitterOption, expectedValue: "github.com/Azure/azure-sdk-for-go/{service-dir}/{package-dir}", exampleValue: "github.com/Azure/azure-sdk-for-go/{service-dir}/{package-dir}", - condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => - isManagementSDK(tspconfig, context, emitters.go), + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), }, { rule: "tspconfig-go-mgmt-fix-const-stuttering-true", @@ -115,8 +112,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ type: KeyType.EmitterOption, expectedValue: true, exampleValue: true, - condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => - isManagementSDK(tspconfig, context, emitters.go), + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), }, { rule: "tspconfig-go-mgmt-generate-examples-true", @@ -124,8 +120,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ type: KeyType.EmitterOption, expectedValue: true, exampleValue: true, - condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => - isManagementSDK(tspconfig, context, emitters.go), + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), }, { rule: "tspconfig-go-mgmt-generate-fakes-true", @@ -133,8 +128,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ type: KeyType.EmitterOption, expectedValue: true, exampleValue: true, - condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => - isManagementSDK(tspconfig, context, emitters.go), + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), }, { rule: "tspconfig-go-mgmt-head-as-boolean-true", @@ -142,8 +136,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ type: KeyType.EmitterOption, expectedValue: true, exampleValue: true, - condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => - isManagementSDK(tspconfig, context, emitters.go), + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), }, { rule: "tspconfig-go-mgmt-inject-spans-true", @@ -151,8 +144,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ type: KeyType.EmitterOption, expectedValue: true, exampleValue: true, - condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => - isManagementSDK(tspconfig, context, emitters.go), + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), }, // java { @@ -163,8 +155,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ exampleValue: "azure-aaa", extraExplanation: "The 'package-dir' should be a string that starts with 'azure', followed by one or more '-' segments. Each segment can contains letters, digits, or underscores", - condition: (tspconfig: TypeSpecConfig, _: Rule.RuleContext) => - isAzureSDK(tspconfig, emitters.java), + condition: (_: TypeSpecConfig, _1: Rule.RuleContext) => true, }, // python { @@ -175,8 +166,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ exampleValue: "azure-mgmt-aaa", extraExplanation: "The 'package-dir' should be a string that starts with 'azure-mgmt', followed by 1 or 2 hyphen-separated lowercase alphabetic segments", - condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => - isManagementSDK(tspconfig, context, emitters.python), + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), }, { rule: "tspconfig-python-mgmt-package-name-equal-string", @@ -184,8 +174,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ type: KeyType.EmitterOption, expectedValue: "{package-dir}", exampleValue: "{package-dir}", - condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => - isManagementSDK(tspconfig, context, emitters.python), + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), }, { rule: "tspconfig-python-mgmt-generate-test-true", @@ -193,8 +182,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ type: KeyType.EmitterOption, expectedValue: true, exampleValue: true, - condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => - isManagementSDK(tspconfig, context, emitters.python), + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), }, { rule: "tspconfig-python-mgmt-generate-sample-true", @@ -202,8 +190,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ type: KeyType.EmitterOption, expectedValue: true, exampleValue: true, - condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => - isManagementSDK(tspconfig, context, emitters.python), + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), }, // csharp { @@ -213,8 +200,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ expectedValue: /^Azure\./, exampleValue: "Azure.aaa", extraExplanation: "The 'package-dir' should be a string that starts with 'Azure.'", - condition: (tspconfig: TypeSpecConfig, _: Rule.RuleContext) => - isAzureSDK(tspconfig, emitters.csharp), + condition: (_: TypeSpecConfig, _1: Rule.RuleContext) => true, }, { rule: "tspconfig-csharp-az-namespace-equal-string", @@ -222,8 +208,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ type: KeyType.EmitterOption, expectedValue: "{package-dir}", exampleValue: "{package-dir}", - condition: (tspconfig: TypeSpecConfig, _: Rule.RuleContext) => - isAzureSDK(tspconfig, emitters.csharp), + condition: (_: TypeSpecConfig, _1: Rule.RuleContext) => true, }, { rule: "tspconfig-csharp-az-clear-output-folder-true", @@ -231,8 +216,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ type: KeyType.EmitterOption, expectedValue: true, exampleValue: true, - condition: (tspconfig: TypeSpecConfig, _: Rule.RuleContext) => - isAzureSDK(tspconfig, emitters.csharp), + condition: (_: TypeSpecConfig, _1: Rule.RuleContext) => true, }, { rule: "tspconfig-csharp-mgmt-package-dir-match-pattern", @@ -242,8 +226,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ exampleValue: "Azure.ResourceManager.aaa", extraExplanation: "The 'package-dir' should be a string that starts with 'Azure.ResourceManager.'", - condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => - isManagementSDK(tspconfig, context, emitters.csharp), + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), }, ]; diff --git a/eng/tools/eslint-plugin-tsv/src/utils/rule-creator.ts b/eng/tools/eslint-plugin-tsv/src/utils/rule-creator.ts index ab921cbd8a07..5040e54b07bd 100644 --- a/eng/tools/eslint-plugin-tsv/src/utils/rule-creator.ts +++ b/eng/tools/eslint-plugin-tsv/src/utils/rule-creator.ts @@ -1,6 +1,11 @@ import { Rule } from "eslint"; import { TypeSpecConfig } from "../config/types.js"; -import { CreateCodeGenSDKRuleArgs, KeyType, RuleDocument, RuleInfo } from "../interfaces/rule-interfaces.js"; +import { + CreateCodeGenSDKRuleArgs, + KeyType, + RuleDocument, + RuleInfo, +} from "../interfaces/rule-interfaces.js"; import { defaultMessageId, defaultRuleType, emitters } from "./constants.js"; import { NamedRule } from "../interfaces/named-eslint.js"; import { AST, getStaticYAMLValue } from "yaml-eslint-parser"; @@ -39,18 +44,9 @@ export function createRuleMessages(messageId: string, docs: RuleDocument) { }; } -export function isAzureSDK(tspconfig: TypeSpecConfig, emitterName: string) { - const flavor = tspconfig.options?.[emitterName]?.flavor as string; - return flavor === "azure"; -} - -export function isManagementSDK( - tspconfig: TypeSpecConfig, - context: Rule.RuleContext, - emitterName: string, -) { +export function isManagementSDK(context: Rule.RuleContext) { const filename = context.filename; - return isAzureSDK(tspconfig, emitterName) && filename.includes(".Management"); + return filename.includes(".Management"); } function validateValue( @@ -62,8 +58,7 @@ function validateValue( switch (typeof expected) { case "boolean": case "string": - if (actual !== expected) - context.report({ node, messageId: defaultMessageId }); + if (actual !== expected) context.report({ node, messageId: defaultMessageId }); break; case "object": if (typeof actual !== "string" || !expected.test(actual)) @@ -107,7 +102,12 @@ export function createCodeGenSDKRule(args: CreateCodeGenSDKRuleArgs): NamedRule. for (const segment of args.key.split(".")) { if (option && segment in option) option = option![segment]; } - validateValue(context, node, option as undefined | string | boolean, args.expectedValue); + validateValue( + context, + node, + option as undefined | string | boolean, + args.expectedValue, + ); break; } case KeyType.Parameter: { @@ -119,7 +119,6 @@ export function createCodeGenSDKRule(args: CreateCodeGenSDKRuleArgs): NamedRule. // TODO: log not supported break; } - }, }, }; diff --git a/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts b/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts index aecc83dc8b96..9e0bb1ea2228 100644 --- a/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts @@ -336,11 +336,7 @@ function createEmitterOptionTestCases( rulePath, ruleName, fileName, - yamlContent: createEmitterOptionExample( - emitterName, - { key: key, value: validValue }, - { key: "flavor", value: "azure" }, - ), + yamlContent: createEmitterOptionExample(emitterName, { key: key, value: validValue }), shouldReportError: false, }, { @@ -348,11 +344,7 @@ function createEmitterOptionTestCases( rulePath, ruleName, fileName, - yamlContent: createEmitterOptionExample( - emitterName, - { key: key, value: invalidValue }, - { key: "flavor", value: "azure" }, - ), + yamlContent: createEmitterOptionExample(emitterName, { key: key, value: invalidValue }), shouldReportError: true, }, { @@ -360,7 +352,7 @@ function createEmitterOptionTestCases( rulePath, ruleName, fileName, - yamlContent: createEmitterOptionExample(emitterName, { key: "flavor", value: "azure" }), + yamlContent: createEmitterOptionExample(emitterName), shouldReportError: true, }, ]; diff --git a/eng/tools/typespec-validation/src/index.ts b/eng/tools/typespec-validation/src/index.ts index b72ea7616531..b50531a18f32 100755 --- a/eng/tools/typespec-validation/src/index.ts +++ b/eng/tools/typespec-validation/src/index.ts @@ -1,7 +1,6 @@ import { parseArgs, ParseArgsConfig } from "node:util"; import { CompileRule } from "./rules/compile.js"; import { EmitAutorestRule } from "./rules/emit-autorest.js"; -import { FlavorAzureRule } from "./rules/flavor-azure.js"; import { FolderStructureRule } from "./rules/folder-structure.js"; import { FormatRule } from "./rules/format.js"; import { LinterRulesetRule } from "./rules/linter-ruleset.js"; @@ -43,7 +42,6 @@ export async function main() { new FolderStructureRule(), new NpmPrefixRule(), new EmitAutorestRule(), - new FlavorAzureRule(), new LinterRulesetRule(), new CompileRule(), new FormatRule(), diff --git a/eng/tools/typespec-validation/src/rules/flavor-azure.ts b/eng/tools/typespec-validation/src/rules/flavor-azure.ts deleted file mode 100644 index 1c01955448b3..000000000000 --- a/eng/tools/typespec-validation/src/rules/flavor-azure.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { parse as yamlParse } from "yaml"; -import { Rule } from "../rule.js"; -import { RuleResult } from "../rule-result.js"; -import { TsvHost } from "../tsv-host.js"; - -export class FlavorAzureRule implements Rule { - readonly name = "FlavorAzure"; - - readonly description = "Client emitters must set 'flavor:azure'"; - - async execute(host: TsvHost, folder: string): Promise { - let success = true; - let stdOutput = ""; - let errorOutput = ""; - - const configText = await host.readTspConfig(folder); - const config = yamlParse(configText); - - const options = config?.options; - for (const emitter in options) { - if (this.isClientEmitter(emitter)) { - const flavor = options[emitter]?.flavor; - - stdOutput += `"${emitter}":\n`; - stdOutput += ` flavor: ${flavor}\n`; - - if (flavor !== "azure") { - success = false; - errorOutput += - "tspconfig.yaml must define the following property:\n" + - "\n" + - "options:\n" + - ` "${emitter}":\n` + - " flavor: azure\n\n"; - } - } - } - - return { - success: success, - stdOutput: stdOutput, - errorOutput: errorOutput, - }; - } - - isClientEmitter(name: string): boolean { - const regex = new RegExp( - "^(@azure-tools/typespec-(csharp|java|python|ts)|@typespec/http-client-.+)$", - ); - - return regex.test(name); - } -} diff --git a/eng/tools/typespec-validation/test/flavor-azure.test.ts b/eng/tools/typespec-validation/test/flavor-azure.test.ts deleted file mode 100644 index e282d9364989..000000000000 --- a/eng/tools/typespec-validation/test/flavor-azure.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { describe, it } from "vitest"; -import { FlavorAzureRule } from "../src/rules/flavor-azure.js"; -import { TsvTestHost } from "./tsv-test-host.js"; -import { strict as assert } from "node:assert"; - -describe("flavor-azure", function () { - const clientEmitterNames = [ - "@azure-tools/typespec-csharp", - "@azure-tools/typespec-java", - "@azure-tools/typespec-python", - "@azure-tools/typespec-ts", - "@typespec/http-client-foo", - ]; - - const nonClientEmitterNames = ["@azure-tools/typespec-autorest", "@typespec/openapi3"]; - - clientEmitterNames.forEach(function (emitter) { - it(`should fail if "${emitter}" is missing flavor`, async function () { - let host = new TsvTestHost(); - - host.readTspConfig = async (_folder: string) => ` - options: - "${emitter}": - package-dir: "foo" - `; - const result = await new FlavorAzureRule().execute(host, TsvTestHost.folder); - assert(!result.success); - }); - - it(`should fail if "${emitter}" flavor is not "azure"`, async function () { - let host = new TsvTestHost(); - host.readTspConfig = async (_folder: string) => ` - options: - "${emitter}": - package-dir: "foo" - flavor: not-azure - `; - const result = await new FlavorAzureRule().execute(host, TsvTestHost.folder); - assert(!result.success); - }); - - it(`should succeed if ${emitter} flavor is "azure"`, async function () { - let host = new TsvTestHost(); - host.readTspConfig = async (_folder: string) => ` - options: - "${emitter}": - package-dir: "foo" - flavor: azure - `; - const result = await new FlavorAzureRule().execute(host, TsvTestHost.folder); - assert(result.success); - }); - }); - - nonClientEmitterNames.forEach(function (emitter) { - it(`should succeed if ${emitter} is missing flavor`, async function () { - let host = new TsvTestHost(); - host.readTspConfig = async (_folder: string) => ` - options: - "${emitter}": - azure-resource-provider-folder: "data-plane" - `; - const result = await new FlavorAzureRule().execute(host, TsvTestHost.folder); - assert(result.success); - }); - }); - - it("should succeed if config is empty", async function () { - let host = new TsvTestHost(); - host.readTspConfig = async (_folder: string) => ""; - const result = await new FlavorAzureRule().execute(host, TsvTestHost.folder); - assert(result.success); - }); - - it("should succeed if config has no options", async function () { - let host = new TsvTestHost(); - host.readTspConfig = async (_folder: string) => ` -emit: - - "@azure-tools/typespec-autorest" -`; - const result = await new FlavorAzureRule().execute(host, TsvTestHost.folder); - assert(result.success); - }); -}); From fa6c65a772a608d6117aa088a864094f5d4ad7f4 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 16:17:52 +0800 Subject: [PATCH 83/93] update tsconfig --- eng/tools/typespec-validation/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eng/tools/typespec-validation/tsconfig.json b/eng/tools/typespec-validation/tsconfig.json index c1eaa0805646..81ede7fd4b23 100644 --- a/eng/tools/typespec-validation/tsconfig.json +++ b/eng/tools/typespec-validation/tsconfig.json @@ -4,6 +4,7 @@ "outDir": "./dist", }, "references": [ - { "path": "../suppressions" } + { "path": "../suppressions" }, + { "path": "../eslint-plugin-tsv" }, ] } From 593970c43231d5d60d83fc7c9df14e0525624f92 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 17:34:19 +0800 Subject: [PATCH 84/93] fix --- eng/tools/eslint-plugin-tsv/package.json | 2 +- .../src/rules/tspconfig-validation-rules.ts | 16 +++++++++------- .../typespec-validation/test/tspconfig.test.ts | 11 +++-------- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index b124f469f81f..5c8ca554c3b7 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -2,7 +2,7 @@ "name": "@azure-tools/eslint-plugin-tsv", "private": true, "type": "module", - "main": "src/index.js", + "main": "dist/src/index.js", "dependencies": { "ajv": "^8.17.1", "yaml-eslint-parser": "^1.2.3" diff --git a/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts b/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts index f5628dac4f3e..11174619046f 100644 --- a/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts +++ b/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts @@ -8,13 +8,15 @@ import { TsvHost } from "../tsv-host.js"; import tsvPlugin, { ESLint } from "eslint-plugin-tsv"; async function runESLint(content: string, folder: string, ruleName: string) { - const config = tsvPlugin.configs.recommended; - for (const key in config.rules) { - if (key !== "tsv/" + ruleName) delete config.rules[key]; - } + const cwd = process.cwd(); const eslint = new ESLint({ - cwd: join(__dirname, "../../../../"), - overrideConfig: tsvPlugin.configs.recommended, + cwd, + overrideConfig: { + ...tsvPlugin.configs.recommended, + rules: { + [`${tsvPlugin.name}/${ruleName}`]: "error", + } + }, overrideConfigFile: true, }); const results = await eslint.lintText(content, { filePath: join(folder, "tspconfig.yaml") }); @@ -35,7 +37,7 @@ function convertToOldRules() { const results = await runESLint(configText, folder, rule.name); if (results.length > 0 && results[0].messages.length > 0) { return { - errorOutput: results[0].messages[0].message, + stdOutput: 'Validation failed. ' + results[0].messages[0].message, // Only used to provide suggestion to correct tspconfig success: true, }; diff --git a/eng/tools/typespec-validation/test/tspconfig.test.ts b/eng/tools/typespec-validation/test/tspconfig.test.ts index e7716d587c9a..9b1b3644b261 100644 --- a/eng/tools/typespec-validation/test/tspconfig.test.ts +++ b/eng/tools/typespec-validation/test/tspconfig.test.ts @@ -47,15 +47,10 @@ describe("tspconfig rules", () => { host.readTspConfig = async (_folder: string) => c.tspconfig; const result = await c.rule.execute(host, c.folder); strictEqual(result.success, true); + assert(result.stdOutput && result.stdOutput.length > 0 && result.errorOutput === undefined); assert( - (c.expectedResult && - result.stdOutput && - result.stdOutput.length > 0 && - result.errorOutput === undefined) || - (!c.expectedResult && - result.stdOutput === undefined && - result.errorOutput && - result.errorOutput.length > 0), + (c.expectedResult && result.stdOutput.includes("validation passed")) || + (!c.expectedResult && result.stdOutput.includes("Validation failed. ")), ); }); }); From 8550e880f86a711cbf86eb77ecc1c66d3bedd4ab Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 17:44:07 +0800 Subject: [PATCH 85/93] update --- eng/tools/eslint-plugin-tsv/package.json | 2 +- eng/tools/eslint-plugin-tsv/test/e2e.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index b124f469f81f..5c8ca554c3b7 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -2,7 +2,7 @@ "name": "@azure-tools/eslint-plugin-tsv", "private": true, "type": "module", - "main": "src/index.js", + "main": "dist/src/index.js", "dependencies": { "ajv": "^8.17.1", "yaml-eslint-parser": "^1.2.3" diff --git a/eng/tools/eslint-plugin-tsv/test/e2e.test.ts b/eng/tools/eslint-plugin-tsv/test/e2e.test.ts index e6da7e107a9d..7c8ed1376584 100644 --- a/eng/tools/eslint-plugin-tsv/test/e2e.test.ts +++ b/eng/tools/eslint-plugin-tsv/test/e2e.test.ts @@ -5,7 +5,7 @@ import eslintPluginTsv from "../src/eslint-plugin-tsv.js"; function createESLint() { return new ESLint({ - cwd: join(__dirname, "../../../../"), + cwd: "/", overrideConfig: eslintPluginTsv.configs.recommended, overrideConfigFile: true, }); From 71fec2e9eaae2949f471534da029165f1ab32cbf Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 17:49:09 +0800 Subject: [PATCH 86/93] improve placeholder --- .../src/rules/tspconfig-validation-rules.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts index 83f193b825f7..9eae374011e1 100644 --- a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts +++ b/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts @@ -19,7 +19,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ key: "service-dir", type: KeyType.Parameter, expectedValue: /^sdk\/[^\/]*$/, - exampleValue: "sdk/aaa", + exampleValue: "sdk/placeholder", extraExplanation: "The 'service-dir' should be a string that starts with 'sdk/', followed by zero or more characters that are not a '/', and ends there", condition: (_: TypeSpecConfig, _1: Rule.RuleContext) => true, @@ -62,7 +62,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ key: "package-dir", type: KeyType.EmitterOption, expectedValue: /^arm(?:-[a-z]+)+$/, - exampleValue: "arm-aaa-bbb", + exampleValue: "arm-placeholder-placeholder", extraExplanation: "The 'package-dir' should be a string that starts with 'arm' and is followed by one or more groups of a hyphen (-) and lowercase letters", condition: tsIsManagementCondition, @@ -72,7 +72,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ key: "packageDetails.name", type: KeyType.EmitterOption, expectedValue: /^\@azure\/arm(?:-[a-z]+)+$/, - exampleValue: "@azure/arm-aaa-bbb", + exampleValue: "@azure/arm-placeholder-placeholder", extraExplanation: "The package name should be a string that starts with '@azure/arm' and is followed by one or more groups of a hyphen (-) and lowercase letters", condition: tsIsManagementCondition, @@ -83,7 +83,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ key: "service-dir", type: KeyType.EmitterOption, expectedValue: /^sdk\/resourcemanager\/[^\/]*$/, - exampleValue: "sdk/resourcemanager/aaa", + exampleValue: "sdk/resourcemanager/placeholder", extraExplanation: "The 'service-dir' should be a string that starts with 'sdk/resourcemanager/', followed by zero or more characters that are not a '/', and ends there", condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), @@ -93,7 +93,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ key: "package-dir", type: KeyType.EmitterOption, expectedValue: /^arm[^\/]*$/, - exampleValue: "armaaa", + exampleValue: "armplaceholder", extraExplanation: "The 'package-dir' should be a string that starts with 'arm' and do not contain a forward slash (/) after it", condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), @@ -152,7 +152,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ key: "package-dir", type: KeyType.EmitterOption, expectedValue: /^azure(-\w+)+$/, - exampleValue: "azure-aaa", + exampleValue: "azure-placeholder", extraExplanation: "The 'package-dir' should be a string that starts with 'azure', followed by one or more '-' segments. Each segment can contains letters, digits, or underscores", condition: (_: TypeSpecConfig, _1: Rule.RuleContext) => true, @@ -163,7 +163,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ key: "package-dir", type: KeyType.EmitterOption, expectedValue: /^azure-mgmt(-[a-z]+){1,2}$/, - exampleValue: "azure-mgmt-aaa", + exampleValue: "azure-mgmt-placeholder", extraExplanation: "The 'package-dir' should be a string that starts with 'azure-mgmt', followed by 1 or 2 hyphen-separated lowercase alphabetic segments", condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), @@ -198,7 +198,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ key: "package-dir", type: KeyType.EmitterOption, expectedValue: /^Azure\./, - exampleValue: "Azure.aaa", + exampleValue: "Azure.placeholder", extraExplanation: "The 'package-dir' should be a string that starts with 'Azure.'", condition: (_: TypeSpecConfig, _1: Rule.RuleContext) => true, }, @@ -223,7 +223,7 @@ const args: CreateCodeGenSDKRuleArgs[] = [ key: "package-dir", type: KeyType.EmitterOption, expectedValue: /^Azure\.ResourceManager\./, - exampleValue: "Azure.ResourceManager.aaa", + exampleValue: "Azure.ResourceManager.Placeholder", extraExplanation: "The 'package-dir' should be a string that starts with 'Azure.ResourceManager.'", condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), From 9a378a02cfc1e9f593384f791e2dbb22889a6b10 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 17:59:45 +0800 Subject: [PATCH 87/93] improve message --- eng/tools/eslint-plugin-tsv/package.json | 1 - eng/tools/eslint-plugin-tsv/src/utils/rule-creator.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index 5c8ca554c3b7..9d1d2e017739 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -2,7 +2,6 @@ "name": "@azure-tools/eslint-plugin-tsv", "private": true, "type": "module", - "main": "dist/src/index.js", "dependencies": { "ajv": "^8.17.1", "yaml-eslint-parser": "^1.2.3" diff --git a/eng/tools/eslint-plugin-tsv/src/utils/rule-creator.ts b/eng/tools/eslint-plugin-tsv/src/utils/rule-creator.ts index 5040e54b07bd..36f9571934f3 100644 --- a/eng/tools/eslint-plugin-tsv/src/utils/rule-creator.ts +++ b/eng/tools/eslint-plugin-tsv/src/utils/rule-creator.ts @@ -40,7 +40,7 @@ export function createRule(ruleContext: RuleInfo): NamedRule.RuleModule { export function createRuleMessages(messageId: string, docs: RuleDocument) { return { - [messageId]: `${docs.error}.\n${docs.action}.\n${docs.example}`, + [messageId]: `Error: ${docs.error}.\nAction: ${docs.action}.\nExample: ${docs.example}`, }; } From 01ec98c82745d35f9b5048ab3243dc8d5d44a4ee Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 18:02:20 +0800 Subject: [PATCH 88/93] update --- eng/tools/eslint-plugin-tsv/package.json | 1 + eng/tools/eslint-plugin-tsv/src/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json index 9d1d2e017739..5c8ca554c3b7 100644 --- a/eng/tools/eslint-plugin-tsv/package.json +++ b/eng/tools/eslint-plugin-tsv/package.json @@ -2,6 +2,7 @@ "name": "@azure-tools/eslint-plugin-tsv", "private": true, "type": "module", + "main": "dist/src/index.js", "dependencies": { "ajv": "^8.17.1", "yaml-eslint-parser": "^1.2.3" diff --git a/eng/tools/eslint-plugin-tsv/src/index.ts b/eng/tools/eslint-plugin-tsv/src/index.ts index 6e6e166c1421..166204f4a9e6 100644 --- a/eng/tools/eslint-plugin-tsv/src/index.ts +++ b/eng/tools/eslint-plugin-tsv/src/index.ts @@ -1,3 +1,4 @@ +// Note: This file is a tempory workaround for converting new rules to old rules import { ESLint } from "eslint"; import tsvPlugin from "./eslint-plugin-tsv.js"; From d770c0d5f5b8a3be54c45d81e1b38fd36010280e Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Fri, 24 Jan 2025 18:15:08 +0800 Subject: [PATCH 89/93] improve message --- .../typespec-validation/src/rules/tspconfig-validation-rules.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts b/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts index 11174619046f..6a13f28dd6fa 100644 --- a/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts +++ b/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts @@ -37,7 +37,7 @@ function convertToOldRules() { const results = await runESLint(configText, folder, rule.name); if (results.length > 0 && results[0].messages.length > 0) { return { - stdOutput: 'Validation failed. ' + results[0].messages[0].message, + stdOutput: 'Validation failed.\n' + results[0].messages[0].message, // Only used to provide suggestion to correct tspconfig success: true, }; From 5158a331789b1b33ff9d3f98e8cfb0db6b910ea6 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Sun, 26 Jan 2025 13:27:19 +0800 Subject: [PATCH 90/93] remove new tool and put new code into current TSV --- eng/tools/eslint-plugin-tsv/package.json | 32 --------- eng/tools/eslint-plugin-tsv/src/index.ts | 6 -- .../src/rules/emit-autorest.ts | 59 ----------------- .../src/rules/kebab-case-org.ts | 55 ---------------- eng/tools/eslint-plugin-tsv/src/utils/npm.ts | 32 --------- eng/tools/eslint-plugin-tsv/test/e2e.test.ts | 66 ------------------- .../test/rules/emit-autorest.test.ts | 32 --------- .../test/rules/kebab-case-org.test.ts | 35 ---------- eng/tools/eslint-plugin-tsv/tsconfig.json | 10 --- eng/tools/eslint-plugin-tsv/vitest.config.ts | 10 --- eng/tools/package.json | 1 - eng/tools/typespec-validation/package.json | 8 ++- .../src/config/config-schema.ts | 0 .../eslint-plugin-tsv/src/config/types.ts | 0 .../src/eslint-plugin-tsv.ts | 0 .../src/interfaces/named-eslint.ts | 0 .../src/interfaces/rule-interfaces.ts | 0 .../src/rules/tspconfig-validation-rules.ts | 0 .../eslint-plugin-tsv/src/utils/constants.ts | 0 .../src/utils/rule-creator.ts | 22 ++++--- .../eslint-plugin-tsv/src/utils/rule-doc.ts | 0 .../src}/eslint-plugin-tsv/src/yaml/types.ts | 0 .../tspconfig-options-validation.test.ts | 0 .../eslint-plugin-tsv/test/utils/npm.test.ts | 0 .../src/rules/tspconfig-validation-rules.ts | 4 +- .../test/tspconfig.test.ts | 2 +- eng/tools/typespec-validation/tsconfig.json | 3 +- 27 files changed, 22 insertions(+), 355 deletions(-) delete mode 100644 eng/tools/eslint-plugin-tsv/package.json delete mode 100644 eng/tools/eslint-plugin-tsv/src/index.ts delete mode 100644 eng/tools/eslint-plugin-tsv/src/rules/emit-autorest.ts delete mode 100644 eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts delete mode 100644 eng/tools/eslint-plugin-tsv/src/utils/npm.ts delete mode 100644 eng/tools/eslint-plugin-tsv/test/e2e.test.ts delete mode 100644 eng/tools/eslint-plugin-tsv/test/rules/emit-autorest.test.ts delete mode 100644 eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts delete mode 100644 eng/tools/eslint-plugin-tsv/tsconfig.json delete mode 100644 eng/tools/eslint-plugin-tsv/vitest.config.ts rename eng/tools/{ => typespec-validation/src}/eslint-plugin-tsv/src/config/config-schema.ts (100%) rename eng/tools/{ => typespec-validation/src}/eslint-plugin-tsv/src/config/types.ts (100%) rename eng/tools/{ => typespec-validation/src}/eslint-plugin-tsv/src/eslint-plugin-tsv.ts (100%) rename eng/tools/{ => typespec-validation/src}/eslint-plugin-tsv/src/interfaces/named-eslint.ts (100%) rename eng/tools/{ => typespec-validation/src}/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts (100%) rename eng/tools/{ => typespec-validation/src}/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts (100%) rename eng/tools/{ => typespec-validation/src}/eslint-plugin-tsv/src/utils/constants.ts (100%) rename eng/tools/{ => typespec-validation/src}/eslint-plugin-tsv/src/utils/rule-creator.ts (81%) rename eng/tools/{ => typespec-validation/src}/eslint-plugin-tsv/src/utils/rule-doc.ts (100%) rename eng/tools/{ => typespec-validation/src}/eslint-plugin-tsv/src/yaml/types.ts (100%) rename eng/tools/{ => typespec-validation/src}/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts (100%) rename eng/tools/{ => typespec-validation/src}/eslint-plugin-tsv/test/utils/npm.test.ts (100%) diff --git a/eng/tools/eslint-plugin-tsv/package.json b/eng/tools/eslint-plugin-tsv/package.json deleted file mode 100644 index 5c8ca554c3b7..000000000000 --- a/eng/tools/eslint-plugin-tsv/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "@azure-tools/eslint-plugin-tsv", - "private": true, - "type": "module", - "main": "dist/src/index.js", - "dependencies": { - "ajv": "^8.17.1", - "yaml-eslint-parser": "^1.2.3" - }, - "peerDependencies": { - "eslint": ">=9.0.0" - }, - "devDependencies": { - "@types/node": "^18.19.31", - "@vitest/coverage-v8": "^2.0.4", - "eslint": "^9.17.0", - "memfs": "^4.15.0", - "rimraf": "^5.0.10", - "typescript": "~5.6.2", - "vitest": "^2.0.4" - }, - "scripts": { - "build": "tsc --build", - "cbt": "npm run clean && npm run build && npm run test:ci", - "clean": "rimraf ./dist ./temp", - "test": "vitest", - "test:ci": "vitest run --coverage --reporter=verbose" - }, - "engines": { - "node": ">= 18.0.0" - } -} diff --git a/eng/tools/eslint-plugin-tsv/src/index.ts b/eng/tools/eslint-plugin-tsv/src/index.ts deleted file mode 100644 index 166204f4a9e6..000000000000 --- a/eng/tools/eslint-plugin-tsv/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Note: This file is a tempory workaround for converting new rules to old rules -import { ESLint } from "eslint"; -import tsvPlugin from "./eslint-plugin-tsv.js"; - -export { ESLint }; -export default tsvPlugin; diff --git a/eng/tools/eslint-plugin-tsv/src/rules/emit-autorest.ts b/eng/tools/eslint-plugin-tsv/src/rules/emit-autorest.ts deleted file mode 100644 index b64953d1ccbf..000000000000 --- a/eng/tools/eslint-plugin-tsv/src/rules/emit-autorest.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Ajv } from "ajv"; -import { Rule } from "eslint"; -import { AST, getStaticYAMLValue } from "yaml-eslint-parser"; -import { TypeSpecConfigJsonSchema } from "../config/config-schema.js"; -import { TypeSpecConfig } from "../config/types.js"; -import { NamedRule } from "../interfaces/named-eslint.js"; - -export const rule: NamedRule.RuleModule = { - name: "emit-autorest", - meta: { - type: "problem", - docs: { - description: - "Requires emitter 'typespec-autorest' to be enabled by default, and requires emitted autorest to match content in repo", - }, - schema: [], - messages: { - invalid: "tspconfig.yaml is invalid per the schema: {{errors}}", - missing: - 'tspconfig.yaml must include the following emitter by default:\n\nemit:\n - "@azure-tools/typespec-autorest"', - // disabled: "Path does not match format '.*/specification/{orgName}/': ''{{filename}}'", - // autorestDiff: "Emitted autorest does not match content in repo", - }, - }, - create(context) { - return { - YAMLDocument(node: Rule.Node) { - const yamlDocument = node as unknown as AST.YAMLDocument; - - // If config yaml is empty, use empty object instead of "null" - const config = getStaticYAMLValue(yamlDocument) || {}; - - const ajv = new Ajv(); - const valid = ajv.validate(TypeSpecConfigJsonSchema, config); - - if (!valid) { - context.report({ - node, - messageId: "invalid", - data: { errors: ajv.errorsText(ajv.errors) }, - }); - return; - } - - const typedConfig = config as unknown as TypeSpecConfig; - if (!typedConfig.emit?.includes("@azure-tools/typespec-autorest")) { - // TODO: Move error message to "emit:" node - context.report({ - node, - messageId: "missing", - }); - return; - } - }, - }; - }, -}; - -export default rule; diff --git a/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts b/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts deleted file mode 100644 index fcc81c116138..000000000000 --- a/eng/tools/eslint-plugin-tsv/src/rules/kebab-case-org.ts +++ /dev/null @@ -1,55 +0,0 @@ -import path from "path"; -import { NamedRule } from "../interfaces/named-eslint.js"; - -// Valid: /specification/kebab-case/Kebab.Case/tspconfig.yaml -// Invalid: /specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml - -export const rule: NamedRule.RuleModule = { - name: "kebab-case-org", - meta: { - type: "problem", - docs: { - description: - "Requires kebab-case for'organization' name (first path segment after 'specification')", - }, - schema: [], - messages: { - invalid: "Path does not match format '.*/specification/{orgName}/': ''{{filename}}'", - kebab: - "Organization name (first path segment after 'specification') does not use kebab-case: '{{orgName}}'", - }, - }, - create(context) { - return { - Program(node) { - const filename = path.resolve(context.filename as string); - const pathSegments = filename.split(path.sep); - const specificationIndex = pathSegments.indexOf("specification"); - const pathValid = specificationIndex >= 0 && specificationIndex < pathSegments.length - 1; - - if (!pathValid) { - context.report({ - node, - messageId: "invalid", - data: { filename: filename }, - }); - return; - } - - const orgName = pathSegments[specificationIndex + 1]; - const kebabCaseRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/; - const orgNameKebabCase = orgName.match(kebabCaseRegex); - - if (!orgNameKebabCase) { - context.report({ - node, - messageId: "kebab", - data: { orgName: orgName }, - }); - } - }, - }; - }, -}; - -export default rule; diff --git a/eng/tools/eslint-plugin-tsv/src/utils/npm.ts b/eng/tools/eslint-plugin-tsv/src/utils/npm.ts deleted file mode 100644 index 0d36838a1f8b..000000000000 --- a/eng/tools/eslint-plugin-tsv/src/utils/npm.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { dirname, join, resolve } from "path"; -import { stat, access } from "fs/promises"; - -export class Npm { - // Simulates `npm prefix` by finding the nearest parent directory containing `package.json` or `node_modules`. - // If neither exist in any parent directories, returns the directory containing the path itself. - // Always returns an absolute path. - static async prefix(path: string): Promise { - path = resolve(path); - - const initialDir = (await stat(path)).isDirectory() ? path : dirname(path); - - for ( - var currentDir = initialDir; - dirname(currentDir) != currentDir; - currentDir = dirname(currentDir) - ) { - try { - await access(join(currentDir, "package.json")); - return currentDir; - } catch {} - - try { - await access(join(currentDir, "node_modules")); - return currentDir; - } catch {} - } - - // Neither found in an parent dir - return initialDir; - } -} diff --git a/eng/tools/eslint-plugin-tsv/test/e2e.test.ts b/eng/tools/eslint-plugin-tsv/test/e2e.test.ts deleted file mode 100644 index 7c8ed1376584..000000000000 --- a/eng/tools/eslint-plugin-tsv/test/e2e.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { ESLint } from "eslint"; -import { join, resolve } from "path"; -import { describe, expect, it } from "vitest"; -import eslintPluginTsv from "../src/eslint-plugin-tsv.js"; - -function createESLint() { - return new ESLint({ - cwd: "/", - overrideConfig: eslintPluginTsv.configs.recommended, - overrideConfigFile: true, - }); -} - -describe("lint-text", () => { - it("Not-Kebab-Case/Not.KebabCase", async () => { - const filePath = "/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml"; - const eslint = createESLint(); - - const results = await eslint.lintText("", { filePath: filePath }); - - expect(results).toHaveLength(1); - expect(results[0].filePath).toBe(filePath); - expect(results[0].messages[0].ruleId).toBe("tsv/kebab-case-org"); - expect(results[0].messages[0].messageId).toBe("kebab"); - }); - - it("Not-Kebab-Case-Disabled/Not.KebabCase", async () => { - const filePath = "/specification/Not-Kebab-Case-Disabled/Not.KebabCase/tspconfig.yaml"; - const eslint = createESLint(); - - const results = await eslint.lintText( - "# eslint-disable tsv/kebab-case-org, tsv/emit-autorest\n", - { - filePath: filePath, - }, - ); - - expect(results).toHaveLength(1); - expect(results[0].filePath).toBe(filePath); - expect(results[0].messages).toHaveLength(0); - }); -}); - -describe("lint-files", () => { - const specsFolder = resolve(__filename, "../../../../../specification"); - - it("contosowidgetmanager/Contso.WidgetManager", async () => { - const eslint = createESLint(); - const filePath = join(specsFolder, "contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml"); - const results = await eslint.lintFiles(filePath); - - expect(results).toHaveLength(1); - expect(results[0].filePath).toBe(filePath); - expect(results[0].messages).toHaveLength(0); - }); - - it("contosowidgetmanager/Contso.Management", async () => { - const eslint = createESLint(); - const filePath = join(specsFolder, "contosowidgetmanager/Contoso.Management/tspconfig.yaml"); - const results = await eslint.lintFiles(filePath); - - expect(results).toHaveLength(1); - expect(results[0].filePath).toBe(filePath); - expect(results[0].messages).toHaveLength(0); - }); -}); diff --git a/eng/tools/eslint-plugin-tsv/test/rules/emit-autorest.test.ts b/eng/tools/eslint-plugin-tsv/test/rules/emit-autorest.test.ts deleted file mode 100644 index 7f8938378634..000000000000 --- a/eng/tools/eslint-plugin-tsv/test/rules/emit-autorest.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Rule, RuleTester } from "eslint"; -import { test } from "vitest"; -import parser from "yaml-eslint-parser"; - -import emitAutorest from "../../src/rules/emit-autorest.js"; - -test("RuleTester", () => { - const ruleTester = new RuleTester({ - languageOptions: { - parser: parser, - }, - }); - - ruleTester.run(emitAutorest.name, emitAutorest as Rule.RuleModule, { - valid: [ - { - code: 'emit:\n - "@azure-tools/typespec-autorest"', - }, - ], - invalid: [ - { - code: "", - errors: [{ messageId: "missing" }], - }, - { - code: "emit:\n - foo", - errors: [{ messageId: "missing" }], - }, - { code: "not: valid", errors: [{ messageId: "invalid" }] }, - ], - }); -}); diff --git a/eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts b/eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts deleted file mode 100644 index b7a0092f3282..000000000000 --- a/eng/tools/eslint-plugin-tsv/test/rules/kebab-case-org.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Rule, RuleTester } from "eslint"; -import { test } from "vitest"; -import parser from "yaml-eslint-parser"; - -import kebabCaseOrg from "../../src/rules/kebab-case-org.js"; - -test("RuleTester", () => { - const ruleTester = new RuleTester({ - languageOptions: { - parser: parser, - }, - }); - - ruleTester.run(kebabCaseOrg.name, kebabCaseOrg as Rule.RuleModule, { - valid: [ - { code: "", filename: "/specification/contoso/Contoso.WidgetManager/tspconfig.yaml" }, - { - code: `# eslint-disable rule-to-test/${kebabCaseOrg.name}`, - filename: "/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", - }, - ], - invalid: [ - { - code: "", - filename: "/specification/Not-Kebab-Case/Not.KebabCase/tspconfig.yaml", - errors: [{ messageId: "kebab" }], - }, - { - code: "", - filename: "tspconfig.yaml", - errors: [{ messageId: "invalid" }], - }, - ], - }); -}); diff --git a/eng/tools/eslint-plugin-tsv/tsconfig.json b/eng/tools/eslint-plugin-tsv/tsconfig.json deleted file mode 100644 index c16578a92bf1..000000000000 --- a/eng/tools/eslint-plugin-tsv/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "./dist" - }, - "include": [ - "src/**/*.ts", - "test/**/*.ts" - ] -} diff --git a/eng/tools/eslint-plugin-tsv/vitest.config.ts b/eng/tools/eslint-plugin-tsv/vitest.config.ts deleted file mode 100644 index 785acc9b7335..000000000000 --- a/eng/tools/eslint-plugin-tsv/vitest.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - coverage: { - include: ["src"], - exclude: ["src/interfaces"], - }, - }, -}); diff --git a/eng/tools/package.json b/eng/tools/package.json index 4e1e559da5a0..1385766802eb 100644 --- a/eng/tools/package.json +++ b/eng/tools/package.json @@ -1,7 +1,6 @@ { "name": "azure-rest-api-specs-eng-tools", "devDependencies": { - "@azure-tools/eslint-plugin-tsv": "file:eslint-plugin-tsv", "@azure-tools/specs-model": "file:specs-model", "@azure-tools/suppressions": "file:suppressions", "@azure-tools/tsp-client-tests": "file:tsp-client-tests", diff --git a/eng/tools/typespec-validation/package.json b/eng/tools/typespec-validation/package.json index c2516ce524f2..b8fcfa146932 100644 --- a/eng/tools/typespec-validation/package.json +++ b/eng/tools/typespec-validation/package.json @@ -10,14 +10,16 @@ "globby": "^14.0.1", "simple-git": "^3.24.0", "suppressions": "file:../suppressions", - "eslint-plugin-tsv": "file:../eslint-plugin-tsv", - "yaml": "^2.4.2" + "yaml": "^2.4.2", + "ajv": "^8.17.1", + "yaml-eslint-parser": "^1.2.3" }, "devDependencies": { "@types/node": "^18.19.31", "@vitest/coverage-v8": "^3.0.2", "typescript": "~5.6.2", - "vitest": "^3.0.2" + "vitest": "^3.0.2", + "eslint": "^9.17.0" }, "scripts": { "build": "tsc --build", diff --git a/eng/tools/eslint-plugin-tsv/src/config/config-schema.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/config/config-schema.ts similarity index 100% rename from eng/tools/eslint-plugin-tsv/src/config/config-schema.ts rename to eng/tools/typespec-validation/src/eslint-plugin-tsv/src/config/config-schema.ts diff --git a/eng/tools/eslint-plugin-tsv/src/config/types.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/config/types.ts similarity index 100% rename from eng/tools/eslint-plugin-tsv/src/config/types.ts rename to eng/tools/typespec-validation/src/eslint-plugin-tsv/src/config/types.ts diff --git a/eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/eslint-plugin-tsv.ts similarity index 100% rename from eng/tools/eslint-plugin-tsv/src/eslint-plugin-tsv.ts rename to eng/tools/typespec-validation/src/eslint-plugin-tsv/src/eslint-plugin-tsv.ts diff --git a/eng/tools/eslint-plugin-tsv/src/interfaces/named-eslint.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/interfaces/named-eslint.ts similarity index 100% rename from eng/tools/eslint-plugin-tsv/src/interfaces/named-eslint.ts rename to eng/tools/typespec-validation/src/eslint-plugin-tsv/src/interfaces/named-eslint.ts diff --git a/eng/tools/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts similarity index 100% rename from eng/tools/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts rename to eng/tools/typespec-validation/src/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts diff --git a/eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts similarity index 100% rename from eng/tools/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts rename to eng/tools/typespec-validation/src/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts diff --git a/eng/tools/eslint-plugin-tsv/src/utils/constants.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/utils/constants.ts similarity index 100% rename from eng/tools/eslint-plugin-tsv/src/utils/constants.ts rename to eng/tools/typespec-validation/src/eslint-plugin-tsv/src/utils/constants.ts diff --git a/eng/tools/eslint-plugin-tsv/src/utils/rule-creator.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/utils/rule-creator.ts similarity index 81% rename from eng/tools/eslint-plugin-tsv/src/utils/rule-creator.ts rename to eng/tools/typespec-validation/src/eslint-plugin-tsv/src/utils/rule-creator.ts index 36f9571934f3..889c4efbbd0e 100644 --- a/eng/tools/eslint-plugin-tsv/src/utils/rule-creator.ts +++ b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/utils/rule-creator.ts @@ -25,12 +25,16 @@ export function createRule(ruleContext: RuleInfo): NamedRule.RuleModule { create(context) { return { YAMLDocument(node: Rule.Node) { - const yamlDocument = node as unknown as AST.YAMLDocument; - const rawConfig = getStaticYAMLValue(yamlDocument) || {}; - const config = rawConfig as unknown as TypeSpecConfig; - - if (!ruleContext.functions.condition(config, context)) return; - ruleContext.functions.validation(config, context, node); + // TODO: remove try-catch block when ESLint based TSV is ready, and have confidence for this + try { + const yamlDocument = node as unknown as AST.YAMLDocument; + const rawConfig = getStaticYAMLValue(yamlDocument) || {}; + const config = rawConfig as unknown as TypeSpecConfig; + if (!ruleContext.functions.condition(config, context)) return; + ruleContext.functions.validation(config, context, node); + } catch (error) { + console.error(`Failed to validate rule '${ruleContext.name}' due to error: ${error}`); + } }, }; }, @@ -40,7 +44,7 @@ export function createRule(ruleContext: RuleInfo): NamedRule.RuleModule { export function createRuleMessages(messageId: string, docs: RuleDocument) { return { - [messageId]: `Error: ${docs.error}.\nAction: ${docs.action}.\nExample: ${docs.example}`, + [messageId]: `Error: ${docs.error}.\nAction: ${docs.action}.\nExample:\n\`\`\`\n${docs.example}\n\`\`\``, }; } @@ -68,7 +72,7 @@ function validateValue( context.report({ node, messageId: defaultMessageId }); break; default: - // TODO: log not supported + console.warn("Unsupported expected-value-type for tspconfig.yaml"); break; } } @@ -116,7 +120,7 @@ export function createCodeGenSDKRule(args: CreateCodeGenSDKRuleArgs): NamedRule. break; } default: - // TODO: log not supported + console.warn("Unsupported key type in tspconfig.yaml"); break; } }, diff --git a/eng/tools/eslint-plugin-tsv/src/utils/rule-doc.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/utils/rule-doc.ts similarity index 100% rename from eng/tools/eslint-plugin-tsv/src/utils/rule-doc.ts rename to eng/tools/typespec-validation/src/eslint-plugin-tsv/src/utils/rule-doc.ts diff --git a/eng/tools/eslint-plugin-tsv/src/yaml/types.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/yaml/types.ts similarity index 100% rename from eng/tools/eslint-plugin-tsv/src/yaml/types.ts rename to eng/tools/typespec-validation/src/eslint-plugin-tsv/src/yaml/types.ts diff --git a/eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts similarity index 100% rename from eng/tools/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts rename to eng/tools/typespec-validation/src/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts diff --git a/eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/test/utils/npm.test.ts similarity index 100% rename from eng/tools/eslint-plugin-tsv/test/utils/npm.test.ts rename to eng/tools/typespec-validation/src/eslint-plugin-tsv/test/utils/npm.test.ts diff --git a/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts b/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts index 6a13f28dd6fa..0929c2699065 100644 --- a/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts +++ b/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts @@ -4,8 +4,8 @@ import { join } from "path"; import { Rule } from "../rule.js"; import { RuleResult } from "../rule-result.js"; import { TsvHost } from "../tsv-host.js"; - -import tsvPlugin, { ESLint } from "eslint-plugin-tsv"; +import { ESLint } from "eslint"; +import tsvPlugin from "../eslint-plugin-tsv/src/eslint-plugin-tsv.js"; async function runESLint(content: string, folder: string, ruleName: string) { const cwd = process.cwd(); diff --git a/eng/tools/typespec-validation/test/tspconfig.test.ts b/eng/tools/typespec-validation/test/tspconfig.test.ts index 9b1b3644b261..9f34671e4f1b 100644 --- a/eng/tools/typespec-validation/test/tspconfig.test.ts +++ b/eng/tools/typespec-validation/test/tspconfig.test.ts @@ -50,7 +50,7 @@ describe("tspconfig rules", () => { assert(result.stdOutput && result.stdOutput.length > 0 && result.errorOutput === undefined); assert( (c.expectedResult && result.stdOutput.includes("validation passed")) || - (!c.expectedResult && result.stdOutput.includes("Validation failed. ")), + (!c.expectedResult && result.stdOutput.includes("Validation failed.")) ); }); }); diff --git a/eng/tools/typespec-validation/tsconfig.json b/eng/tools/typespec-validation/tsconfig.json index 81ede7fd4b23..c1eaa0805646 100644 --- a/eng/tools/typespec-validation/tsconfig.json +++ b/eng/tools/typespec-validation/tsconfig.json @@ -4,7 +4,6 @@ "outDir": "./dist", }, "references": [ - { "path": "../suppressions" }, - { "path": "../eslint-plugin-tsv" }, + { "path": "../suppressions" } ] } From 718cb300baf0370707b66ce1f2b5216840c9beb8 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Sun, 26 Jan 2025 13:32:00 +0800 Subject: [PATCH 91/93] revert tsconfig --- eng/tools/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/eng/tools/tsconfig.json b/eng/tools/tsconfig.json index 44787ca9f682..eca3d4cdace7 100644 --- a/eng/tools/tsconfig.json +++ b/eng/tools/tsconfig.json @@ -11,7 +11,6 @@ "composite": true, }, "references": [ - { "path": "./eslint-plugin-tsv" }, { "path": "./specs-model" }, { "path": "./suppressions" }, { "path": "./tsp-client-tests" }, From 06435c36dfb1be966d575c796e19bb8f31429914 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Sun, 26 Jan 2025 13:39:13 +0800 Subject: [PATCH 92/93] removed unnecessary rules --- .../src/eslint-plugin-tsv.ts | 12 +--- .../eslint-plugin-tsv/test/utils/npm.test.ts | 56 ------------------- 2 files changed, 2 insertions(+), 66 deletions(-) delete mode 100644 eng/tools/typespec-validation/src/eslint-plugin-tsv/test/utils/npm.test.ts diff --git a/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/eslint-plugin-tsv.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/eslint-plugin-tsv.ts index 027cee59ab7a..581472b85320 100644 --- a/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/eslint-plugin-tsv.ts +++ b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -1,16 +1,11 @@ import parser from "yaml-eslint-parser"; import { NamedESLint } from "./interfaces/named-eslint.js"; -import emitAutorest from "./rules/emit-autorest.js"; -import kebabCaseOrg from "./rules/kebab-case-org.js"; import tspconfigValidationRules from "./rules/tspconfig-validation-rules.js"; const plugin: NamedESLint.Plugin = { configs: { recommended: {} }, name: "tsv", - rules: { - [kebabCaseOrg.name]: kebabCaseOrg, - [emitAutorest.name]: emitAutorest, - }, + rules: {}, }; plugin.configs.recommended = { @@ -18,10 +13,7 @@ plugin.configs.recommended = { [plugin.name]: plugin, }, files: ["*.yaml", "**/*.yaml"], - rules: { - [`${plugin.name}/${kebabCaseOrg.name}`]: "error", - [`${plugin.name}/${emitAutorest.name}`]: "error", - }, + rules: {}, languageOptions: { parser: parser, }, diff --git a/eng/tools/typespec-validation/src/eslint-plugin-tsv/test/utils/npm.test.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/test/utils/npm.test.ts deleted file mode 100644 index 28e54924170a..000000000000 --- a/eng/tools/typespec-validation/src/eslint-plugin-tsv/test/utils/npm.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { vol } from "memfs"; -import { resolve } from "path"; -import { Npm } from "../../src/utils/npm.js"; - -vi.mock("fs/promises", async () => { - const memfs = await import("memfs"); - return { - ...memfs.fs.promises, - }; -}); - -describe("prefix", () => { - beforeEach(() => { - vol.reset(); - }); - - describe("returns current directory if no match", () => { - it.each([ - ["/foo/bar/tspconfig.yaml", "/foo/bar"], - ["/foo/bar", "/foo/bar"], - ])("%s", async (path, expected) => { - vol.fromJSON({ - "/foo/bar/tspconfig.yaml": "", - }); - - expect(await Npm.prefix(path)).toBe(resolve(expected)); - }); - }); - - describe("returns first match", () => { - it.each([ - ["/pj", "/pj"], - ["/pj/none", "/pj"], - ["/pj/none/none/none", "/pj"], - ["/pj/nm", "/pj/nm"], - ["/pj/nm/none", "/pj/nm"], - ["/pj/pj", "/pj/pj"], - ["/pj/nm/pj", "/pj/nm/pj"], - ["/pj/pj/nm", "/pj/pj/nm"], - ])("%s", async (path, expected) => { - vol.fromJSON({ - "/pj/package.json": "", - "/pj/none": null, - "/pj/none/none/none": null, - "/pj/nm/node_modules": null, - "/pj/nm/none": null, - "/pj/pj/package.json": "", - "/pj/nm/pj/package.json": "", - "/pj/pj/nm/node_modules": null, - }); - - expect(await Npm.prefix(path)).toBe(resolve(expected)); - }); - }); -}); From fd196a1fbf00d20c4dfa1097ccdcb08052762b3f Mon Sep 17 00:00:00 2001 From: Ray Chen Date: Wed, 5 Feb 2025 14:35:27 -0800 Subject: [PATCH 93/93] Set up CI with Azure Pipelines [skip ci] --- azure-pipeline.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 azure-pipeline.yml diff --git a/azure-pipeline.yml b/azure-pipeline.yml new file mode 100644 index 000000000000..7a16b34b13ed --- /dev/null +++ b/azure-pipeline.yml @@ -0,0 +1,26 @@ +name: "spec-gen-sdk-all-specs" + +pool: + name: azsdk-pool-mms-ubuntu-2004-general + vmImage: 'Ubuntu-20.04' + +trigger: + branches: + include: + - master + +pr: + autoCancel: false + +variables: + TRAVIS: 'true' + TRAVIS_BRANCH: $(System.PullRequest.TargetBranch) + TRAVIS_PULL_REQUEST: $(System.PullRequest.PullRequestNumber) + TRAVIS_REPO_SLUG: $(Build.Repository.Name) + TRAVIS_PULL_REQUEST_SLUG: $(Build.Repository.Name) + TRAVIS_PULL_REQUEST_SHA: $(Build.SourceVersion) + PR_ONLY: 'true' + +jobs: +- template: .azure-pipelines/BranchProtectionForPrivateRepo.yml +- template: .azure-pipelines/ShouldSendPRToMain.yml