diff --git a/eng/tools/typespec-validation/package.json b/eng/tools/typespec-validation/package.json index c4cdbafaa805..b8fcfa146932 100644 --- a/eng/tools/typespec-validation/package.json +++ b/eng/tools/typespec-validation/package.json @@ -10,13 +10,16 @@ "globby": "^14.0.1", "simple-git": "^3.24.0", "suppressions": "file:../suppressions", - "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/typespec-validation/src/eslint-plugin-tsv/src/config/config-schema.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/config/config-schema.ts new file mode 100644 index 000000000000..e92b473ebf20 --- /dev/null +++ b/eng/tools/typespec-validation/src/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/typespec-validation/src/eslint-plugin-tsv/src/config/types.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/config/types.ts new file mode 100644 index 000000000000..575e3b6df553 --- /dev/null +++ b/eng/tools/typespec-validation/src/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/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 new file mode 100644 index 000000000000..581472b85320 --- /dev/null +++ b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/eslint-plugin-tsv.ts @@ -0,0 +1,27 @@ +import parser from "yaml-eslint-parser"; +import { NamedESLint } from "./interfaces/named-eslint.js"; +import tspconfigValidationRules from "./rules/tspconfig-validation-rules.js"; + +const plugin: NamedESLint.Plugin = { + configs: { recommended: {} }, + name: "tsv", + rules: {}, +}; + +plugin.configs.recommended = { + plugins: { + [plugin.name]: plugin, + }, + files: ["*.yaml", "**/*.yaml"], + rules: {}, + languageOptions: { + parser: parser, + }, +}; + +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/typespec-validation/src/eslint-plugin-tsv/src/interfaces/named-eslint.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/interfaces/named-eslint.ts new file mode 100644 index 000000000000..a09d8d27f150 --- /dev/null +++ b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/interfaces/named-eslint.ts @@ -0,0 +1,17 @@ +import { ESLint, Linter, 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 { + configs: { recommended: Linter.Config }; + name: string; + rules?: Record; + } +} diff --git a/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts new file mode 100644 index 000000000000..d45d6ab20597 --- /dev/null +++ b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/interfaces/rule-interfaces.ts @@ -0,0 +1,34 @@ +import { Rule } from "eslint"; +import { TypeSpecConfig } from "../config/types.js"; + +export enum KeyType { + EmitterOption, + Parameter, +} + +export interface RuleDocument { + description: string; + error: string; + action: string; + example: string; +} + +export interface RuleInfo { + name: string; + documentation: RuleDocument; + functions: { + messages: () => { [messageId: string]: string } | undefined; + condition: (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => boolean; + 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/typespec-validation/src/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts new file mode 100644 index 000000000000..9eae374011e1 --- /dev/null +++ b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/rules/tspconfig-validation-rules.ts @@ -0,0 +1,233 @@ +import { Rule } from "eslint"; +import { TypeSpecConfig } from "../config/types.js"; +import { createCodeGenSDKRule, isManagementSDK } from "../utils/rule-creator.js"; +import { emitters } from "../utils/constants.js"; +import { CreateCodeGenSDKRuleArgs, KeyType } from "../interfaces/rule-interfaces.js"; + +const tsIsManagementCondition = (tspconfig: TypeSpecConfig, context: Rule.RuleContext) => { + const emitterName = emitters.ts; + const isModularLibrary = tspconfig.options?.[emitterName]?.isModularLibrary as + | boolean + | undefined; + return isManagementSDK(context) && isModularLibrary !== false; +}; + +const args: CreateCodeGenSDKRuleArgs[] = [ + // common + { + rule: "tspconfig-common-az-service-dir-match-pattern", + key: "service-dir", + type: KeyType.Parameter, + expectedValue: /^sdk\/[^\/]*$/, + 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, + }, + // ts + { + 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-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, + }, + { + rule: "tspconfig-ts-mgmt-modular-package-name-match-pattern", + key: "packageDetails.name", + type: KeyType.EmitterOption, + expectedValue: /^\@azure\/arm(?:-[a-z]+)+$/, + 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, + }, + // go + { + rule: "tspconfig-go-mgmt-service-dir-match-pattern", + key: "service-dir", + type: KeyType.EmitterOption, + expectedValue: /^sdk\/resourcemanager\/[^\/]*$/, + 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), + }, + { + rule: "tspconfig-go-mgmt-package-dir-match-pattern", + key: "package-dir", + type: KeyType.EmitterOption, + expectedValue: /^arm[^\/]*$/, + 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), + }, + { + 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: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), + }, + { + rule: "tspconfig-go-mgmt-fix-const-stuttering-true", + key: "fix-const-stuttering", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), + }, + { + rule: "tspconfig-go-mgmt-generate-examples-true", + key: "generate-examples", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), + }, + { + rule: "tspconfig-go-mgmt-generate-fakes-true", + key: "generate-fakes", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), + }, + { + rule: "tspconfig-go-mgmt-head-as-boolean-true", + key: "head-as-boolean", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), + }, + { + rule: "tspconfig-go-mgmt-inject-spans-true", + key: "inject-spans", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), + }, + // java + { + rule: "tspconfig-java-az-package-dir-match-pattern", + key: "package-dir", + type: KeyType.EmitterOption, + expectedValue: /^azure(-\w+)+$/, + 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, + }, + // 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-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), + }, + { + rule: "tspconfig-python-mgmt-package-name-equal-string", + key: "package-name", + type: KeyType.EmitterOption, + expectedValue: "{package-dir}", + exampleValue: "{package-dir}", + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), + }, + { + rule: "tspconfig-python-mgmt-generate-test-true", + key: "generate-test", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), + }, + { + rule: "tspconfig-python-mgmt-generate-sample-true", + key: "generate-sample", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), + }, + // csharp + { + rule: "tspconfig-csharp-az-package-dir-match-pattern", + key: "package-dir", + type: KeyType.EmitterOption, + expectedValue: /^Azure\./, + exampleValue: "Azure.placeholder", + extraExplanation: "The 'package-dir' should be a string that starts with 'Azure.'", + condition: (_: TypeSpecConfig, _1: Rule.RuleContext) => true, + }, + { + rule: "tspconfig-csharp-az-namespace-equal-string", + key: "namespace", + type: KeyType.EmitterOption, + expectedValue: "{package-dir}", + exampleValue: "{package-dir}", + condition: (_: TypeSpecConfig, _1: Rule.RuleContext) => true, + }, + { + rule: "tspconfig-csharp-az-clear-output-folder-true", + key: "clear-output-folder", + type: KeyType.EmitterOption, + expectedValue: true, + exampleValue: true, + condition: (_: TypeSpecConfig, _1: Rule.RuleContext) => true, + }, + { + rule: "tspconfig-csharp-mgmt-package-dir-match-pattern", + key: "package-dir", + type: KeyType.EmitterOption, + expectedValue: /^Azure\.ResourceManager\./, + exampleValue: "Azure.ResourceManager.Placeholder", + extraExplanation: + "The 'package-dir' should be a string that starts with 'Azure.ResourceManager.'", + condition: (_: TypeSpecConfig, context: Rule.RuleContext) => isManagementSDK(context), + }, +]; + +export default args.map((a) => createCodeGenSDKRule(a)); diff --git a/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/utils/constants.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/utils/constants.ts new file mode 100644 index 000000000000..aff4758de554 --- /dev/null +++ b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/utils/constants.ts @@ -0,0 +1,13 @@ +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", + common: "", +}; + +export const defaultMessageId = "problem"; + +export const defaultRuleType = "problem"; diff --git a/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/utils/rule-creator.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/utils/rule-creator.ts new file mode 100644 index 000000000000..889c4efbbd0e --- /dev/null +++ b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/utils/rule-creator.ts @@ -0,0 +1,131 @@ +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) { + // 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}`); + } + }, + }; + }, + }; + return rule; +} + +export function createRuleMessages(messageId: string, docs: RuleDocument) { + return { + [messageId]: `Error: ${docs.error}.\nAction: ${docs.action}.\nExample:\n\`\`\`\n${docs.example}\n\`\`\``, + }; +} + +export function isManagementSDK(context: Rule.RuleContext) { + const filename = context.filename; + return 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: + console.warn("Unsupported expected-value-type for tspconfig.yaml"); + 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: + console.warn("Unsupported key type in tspconfig.yaml"); + break; + } + }, + }, + }; + + return createRule(ruleInfo); +} diff --git a/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/utils/rule-doc.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/utils/rule-doc.ts new file mode 100644 index 000000000000..1ffef72bee77 --- /dev/null +++ b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/utils/rule-doc.ts @@ -0,0 +1,113 @@ +import { KeyType, RuleDocument } from "../interfaces/rule-interfaces.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.${emitterName}.${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/typespec-validation/src/eslint-plugin-tsv/src/yaml/types.ts b/eng/tools/typespec-validation/src/eslint-plugin-tsv/src/yaml/types.ts new file mode 100644 index 000000000000..2c73c7c0da1e --- /dev/null +++ b/eng/tools/typespec-validation/src/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/typespec-validation/src/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 new file mode 100644 index 000000000000..9e0bb1ea2228 --- /dev/null +++ b/eng/tools/typespec-validation/src/eslint-plugin-tsv/test/rules/tspconfig-options-validation.test.ts @@ -0,0 +1,397 @@ +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 { NamedRule } from "../../src/interfaces/named-eslint.js"; +import { createEmitterOptionExample, createParameterExample } from "../../src/utils/rule-doc.js"; + +interface Case { + description: string; + rulePath: string; + ruleName: string; + fileName?: string; + yamlContent: string; + shouldReportError: boolean; +} + +const managementTspconfigPath = "contosowidgetmanager/Contoso.Management/tspconfig.yaml"; +const rulePath = "../../src/rules/tspconfig-validation-rules.js"; + +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", + managementTspconfigPath, + "generateMetadata", + true, + false, +); + +const tsManagementHierarchyClientTestCases = createEmitterOptionTestCases( + emitters.ts, + rulePath, + "tspconfig-ts-mgmt-modular-hierarchy-client-false", + managementTspconfigPath, + "hierarchyClient", + false, + true, +); + +const tsManagementExperimentalExtensibleEnumsTestCases = createEmitterOptionTestCases( + emitters.ts, + rulePath, + "tspconfig-ts-mgmt-modular-experimental-extensible-enums-true", + managementTspconfigPath, + "experimentalExtensibleEnums", + true, + false, +); + +const tsManagementEnableOperationGroupTestCases = createEmitterOptionTestCases( + emitters.ts, + rulePath, + "tspconfig-ts-mgmt-modular-enable-operation-group-true", + managementTspconfigPath, + "enableOperationGroup", + true, + false, +); + +const tsManagementPackageDirTestCases = createEmitterOptionTestCases( + emitters.ts, + rulePath, + "tspconfig-ts-mgmt-modular-package-dir-match-pattern", + managementTspconfigPath, + "package-dir", + "arm-aaa-bbb", + "aaa-bbb", +); + +const tsManagementPackageNameTestCases = createEmitterOptionTestCases( + emitters.ts, + rulePath, + "tspconfig-ts-mgmt-modular-package-name-match-pattern", + managementTspconfigPath, + "packageDetails.name", + "@azure/arm-aaa-bbb", + "@azure/aaa-bbb", +); + +const goManagementServiceDirTestCases = createEmitterOptionTestCases( + emitters.go, + rulePath, + "tspconfig-go-mgmt-service-dir-match-pattern", + managementTspconfigPath, + "service-dir", + "sdk/resourcemanager/aaa", + "sdk/manager/aaa", +); + +const goManagementPackageDirTestCases = createEmitterOptionTestCases( + emitters.go, + rulePath, + "tspconfig-go-mgmt-package-dir-match-pattern", + managementTspconfigPath, + "package-dir", + "armaaa", + "aaa", +); + +const goManagementModuleTestCases = createEmitterOptionTestCases( + 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 = createEmitterOptionTestCases( + emitters.go, + rulePath, + "tspconfig-go-mgmt-fix-const-stuttering-true", + managementTspconfigPath, + "fix-const-stuttering", + true, + false, +); + +const goManagementGenerateExamplesTestCases = createEmitterOptionTestCases( + emitters.go, + rulePath, + "tspconfig-go-mgmt-generate-examples-true", + managementTspconfigPath, + "generate-examples", + true, + false, +); + +const goManagementGenerateFakesTestCases = createEmitterOptionTestCases( + emitters.go, + rulePath, + "tspconfig-go-mgmt-generate-fakes-true", + managementTspconfigPath, + "generate-fakes", + true, + false, +); + +const goManagementHeadAsBooleanTestCases = createEmitterOptionTestCases( + emitters.go, + rulePath, + "tspconfig-go-mgmt-head-as-boolean-true", + managementTspconfigPath, + "head-as-boolean", + true, + false, +); + +const goManagementInjectSpansTestCases = createEmitterOptionTestCases( + emitters.go, + rulePath, + "tspconfig-go-mgmt-inject-spans-true", + managementTspconfigPath, + "inject-spans", + true, + false, +); + +const javaManagementPackageDirTestCases = createEmitterOptionTestCases( + emitters.java, + rulePath, + "tspconfig-java-az-package-dir-match-pattern", + "", + "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, + ...tsManagementExperimentalExtensibleEnumsTestCases, + ...tsManagementEnableOperationGroupTestCases, + ...tsManagementPackageDirTestCases, + ...tsManagementPackageNameTestCases, + // go + ...goManagementServiceDirTestCases, + ...goManagementPackageDirTestCases, + ...goManagementModuleTestCases, + ...goManagementFixConstStutteringTestCases, + ...goManagementGenerateExamplesTestCases, + ...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: { + parser: parser, + }, + }); + + const ruleModule = await import(c.rulePath); + const rule = ruleModule.default.find((r: NamedRule.RuleModule) => r.name === c.ruleName); + 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); + }); +}); + +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 }), + shouldReportError: false, + }, + { + description: `invalid: ${key} is ${invalidValue}`, + rulePath, + ruleName, + fileName, + yamlContent: createEmitterOptionExample(emitterName, { key: key, value: invalidValue }), + shouldReportError: true, + }, + { + description: `invalid: ${key} is undefined`, + rulePath, + ruleName, + fileName, + yamlContent: createEmitterOptionExample(emitterName), + 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; +} diff --git a/eng/tools/typespec-validation/src/index.ts b/eng/tools/typespec-validation/src/index.ts index b72ea7616531..b1ac12500eb7 100755 --- a/eng/tools/typespec-validation/src/index.ts +++ b/eng/tools/typespec-validation/src/index.ts @@ -1,13 +1,14 @@ 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"; 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,15 +40,16 @@ export async function main() { return; } - const rules = [ + let rules: Rule[] = [ new FolderStructureRule(), new NpmPrefixRule(), new EmitAutorestRule(), - new FlavorAzureRule(), new LinterRulesetRule(), new CompileRule(), new FormatRule(), ]; + rules.push(...tspconfigRules); + let success = true; for (let i = 0; i < rules.length; i++) { const rule = rules[i]; 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/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/src/rules/tspconfig-validation-rules.ts b/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts new file mode 100644 index 000000000000..0929c2699065 --- /dev/null +++ b/eng/tools/typespec-validation/src/rules/tspconfig-validation-rules.ts @@ -0,0 +1,59 @@ +// Note: temporary workaround to convert new rules to old rules to provides suggestion to correct tspconfig + +import { join } from "path"; +import { Rule } from "../rule.js"; +import { RuleResult } from "../rule-result.js"; +import { TsvHost } from "../tsv-host.js"; +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(); + const eslint = new ESLint({ + cwd, + overrideConfig: { + ...tsvPlugin.configs.recommended, + rules: { + [`${tsvPlugin.name}/${ruleName}`]: "error", + } + }, + overrideConfigFile: true, + }); + 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 ?? {})) { + 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 results = await runESLint(configText, folder, rule.name); + if (results.length > 0 && results[0].messages.length > 0) { + return { + stdOutput: 'Validation failed.\n' + results[0].messages[0].message, + // Only used to provide suggestion to correct tspconfig + success: true, + }; + } + + return { + stdOutput: `[${rule.name}]: validation passed.`, + success: true, + }; + }, + }; + oldRules.push(oldRule); + } + return oldRules; +} + +const rules = convertToOldRules(); + +export default rules; 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); - }); -}); diff --git a/eng/tools/typespec-validation/test/tspconfig.test.ts b/eng/tools/typespec-validation/test/tspconfig.test.ts index 3be4e4f1deb3..9f34671e4f1b 100644 --- a/eng/tools/typespec-validation/test/tspconfig.test.ts +++ b/eng/tools/typespec-validation/test/tspconfig.test.ts @@ -1,6 +1,6 @@ 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"; @@ -13,88 +13,44 @@ 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, - }, - { - rule: new TspConfigJavaPackageDirectoryRule(), - folder: TsvTestHost.folder, - when: "package-dir \"azure-\" is invalid", - tspconfig: ` -options: - "@azure-tools/typespec-java": - package-dir: azure- -`, - 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, TsvTestHost.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/bbb/", + 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-az-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(result.stdOutput && result.stdOutput.length > 0 && result.errorOutput === undefined); + assert( + (c.expectedResult && result.stdOutput.includes("validation passed")) || + (!c.expectedResult && result.stdOutput.includes("Validation failed.")) + ); + }); }); diff --git a/package-lock.json b/package-lock.json index f1ffe4f00aa3..7037acc8b484 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,799 @@ "@azure-tools/typespec-validation": "file:typespec-validation" } }, + "eng/tools/eslint-plugin-tsv": { + "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", + "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", @@ -386,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", @@ -587,6 +1382,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 +3159,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", @@ -4839,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", @@ -5992,6 +6849,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 +7687,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 +9257,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 +9991,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 +10084,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 +10844,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", diff --git a/specification/playwrighttesting/PlaywrightTesting.AuthManager/main.tsp b/specification/playwrighttesting/PlaywrightTesting.AuthManager/main.tsp index 7ee1e26f125f..cb29ac97da9a 100644 --- a/specification/playwrighttesting/PlaywrightTesting.AuthManager/main.tsp +++ b/specification/playwrighttesting/PlaywrightTesting.AuthManager/main.tsp @@ -16,11 +16,11 @@ using Microsoft.PlaywrightTesting.Shared; @server( "{endpoint}", - "Microsoft Azure Playwright Service API Endpoint", + "Microsoft Azure Playwright Service API Endpoint Test", { @doc(""" Supported Azure Playwright Service API Endpoints (protocol and hostname, for example: - https://{region}.api.playwright.microsoft.com). + https://{region}.api.playwright.microsoft.com Test). """) endpoint: string, } diff --git a/specification/playwrighttesting/PlaywrightTesting.AuthManager/tspconfig.yaml b/specification/playwrighttesting/PlaywrightTesting.AuthManager/tspconfig.yaml index 6873e72af011..ae251372cb7d 100644 --- a/specification/playwrighttesting/PlaywrightTesting.AuthManager/tspconfig.yaml +++ b/specification/playwrighttesting/PlaywrightTesting.AuthManager/tspconfig.yaml @@ -5,7 +5,7 @@ linter: - "@azure-tools/typespec-azure-rulesets/data-plane" parameters: "service-dir": - default: "sdk/playwrighttesting" + default: "./sdk/playwrighttesting" "dependencies": "additionalDirectories": - "specification/playwrighttesting/PlaywrightTesting.Shared/" diff --git a/specification/playwrighttesting/PlaywrightTesting.Shared/main.tsp b/specification/playwrighttesting/PlaywrightTesting.Shared/main.tsp index b45ff810de7a..21f84c701332 100644 --- a/specification/playwrighttesting/PlaywrightTesting.Shared/main.tsp +++ b/specification/playwrighttesting/PlaywrightTesting.Shared/main.tsp @@ -11,14 +11,14 @@ using Azure.Core; @versioned(Microsoft.PlaywrightTesting.Shared.Versions) namespace Microsoft.PlaywrightTesting.Shared; -@doc("The PlaywrightTesting Accounts Model Version.") +@doc("The PlaywrightTesting Accounts Model Version. Test") enum Versions { @doc("Version 1.0") @useDependency(Azure.Core.Versions.v1_0_Preview_2) v1_0, } -@doc("An account is a parent resource for most of the other service resources. It's directly mapped to an Azure resource.") +@doc("An account is a parent resource for most of the other service resources. It's directly mapped to an Azure resource. Test") @resource("accounts") model Account { @key("accountId") diff --git a/specification/playwrighttesting/data-plane/Microsoft.PlaywrightTesting.AuthManager/stable/2024-12-01/playwrighttesting.json b/specification/playwrighttesting/data-plane/Microsoft.PlaywrightTesting.AuthManager/stable/2024-12-01/playwrighttesting.json index 5aa973ac936e..a98c790fa748 100644 --- a/specification/playwrighttesting/data-plane/Microsoft.PlaywrightTesting.AuthManager/stable/2024-12-01/playwrighttesting.json +++ b/specification/playwrighttesting/data-plane/Microsoft.PlaywrightTesting.AuthManager/stable/2024-12-01/playwrighttesting.json @@ -19,7 +19,7 @@ { "name": "endpoint", "in": "path", - "description": "Supported Azure Playwright Service API Endpoints (protocol and hostname, for example:\n https://{region}.api.playwright.microsoft.com).", + "description": "Supported Azure Playwright Service API Endpoints (protocol and hostname, for example:\n https://{region}.api.playwright.microsoft.com Test).", "required": true, "type": "string" } @@ -98,11 +98,6 @@ } } } - }, - "x-ms-examples": { - "Accounts_Get": { - "$ref": "./examples/Accounts_Get.json" - } } } }, @@ -154,11 +149,6 @@ } } }, - "x-ms-examples": { - "AccessTokens_List": { - "$ref": "./examples/AccessTokens_List.json" - } - }, "x-ms-pageable": { "nextLinkName": "nextLink" } @@ -221,11 +211,6 @@ } } } - }, - "x-ms-examples": { - "AccessTokens_Get": { - "$ref": "./examples/AccessTokens_Get.json" - } } }, "put": { @@ -306,11 +291,6 @@ } } } - }, - "x-ms-examples": { - "AccessTokens_CreateOrReplace": { - "$ref": "./examples/AccessTokens_CreateOrReplace.json" - } } }, "delete": { @@ -366,11 +346,6 @@ } } } - }, - "x-ms-examples": { - "AccessTokens_Delete": { - "$ref": "./examples/AccessTokens_Delete.json" - } } } }, @@ -456,11 +431,6 @@ } } } - }, - "x-ms-examples": { - "Accounts_GetBrowsers": { - "$ref": "./examples/Accounts_GetBrowsers.json" - } } } } @@ -614,7 +584,7 @@ }, "Microsoft.PlaywrightTesting.Shared.Account": { "type": "object", - "description": "An account is a parent resource for most of the other service resources. It's directly mapped to an Azure resource.", + "description": "An account is a parent resource for most of the other service resources. It's directly mapped to an Azure resource. Test", "properties": { "id": { "type": "string",