-
Notifications
You must be signed in to change notification settings - Fork 5.6k
add TSV rules for CShanpc and fix reslove placeholder variable #38924
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
skywing918
wants to merge
12
commits into
main
Choose a base branch
from
kyle/httpClientCsharpRules
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+481
−15
Open
Changes from 8 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
a86f2d8
add HTTP client C# rules for emitter output directory and namespace v…
c18b3ad
Merge branch 'main' into kyle/httpClientCsharpRules
skywing918 d1d031e
Enhance rule validation by introducing RuleFailureType enum and updat…
b4d5f63
Fix import order for RuleResult and RuleFailureType in sdk-tspconfig-…
540bbdc
Add validation behavior tests for CSharp emitters in sdk-tspconfig-va…
8f0a7ce
Merge branch 'main' into kyle/httpClientCsharpRules
skywing918 40b53e9
Refactor test case creation functions for improved clarity and mainta…
d6f5c38
Merge branch 'kyle/httpClientCsharpRules' of https://github.com/Azure…
b9ee586
Merge branch 'main' into kyle/httpClientCsharpRules
skywing918 99155ab
Refactor RuleResult and related validation logic to remove RuleFailur…
e62a6c5
fix format issue
ca0a1ba
Merge branch 'main' into kyle/httpClientCsharpRules
skywing918 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,13 @@ | ||
| export enum RuleFailureType { | ||
| NotFind = "not_find", | ||
| Mismatch = "mismatch", | ||
| ParseError = "parse_error", | ||
| TypeError = "type_error", | ||
| } | ||
|
|
||
| export interface RuleResult { | ||
| readonly success: boolean; | ||
| readonly type?: RuleFailureType; | ||
| readonly stdOutput?: string; | ||
| readonly errorOutput?: string; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,10 @@ | ||
| /* eslint-disable */ | ||
| // TODO: Enable eslint, fix errors | ||
|
|
||
| import { join } from "path"; | ||
| import { Suppression } from "suppressions"; | ||
| import { parse as yamlParse } from "yaml"; | ||
| import { RuleResult } from "../rule-result.js"; | ||
| import { RuleFailureType, RuleResult } from "../rule-result.js"; | ||
| import { Rule } from "../rule.js"; | ||
| import { fileExists, getSuppressions, readTspConfig } from "../utils.js"; | ||
|
|
||
|
|
@@ -26,6 +26,7 @@ | |
| return this.createFailedResult( | ||
| `Failed to find ${join(folder, "tspconfig.yaml")}`, | ||
| "Please add tspconfig.yaml", | ||
| RuleFailureType.ParseError, | ||
| ); | ||
|
|
||
| let config = undefined; | ||
|
|
@@ -36,6 +37,7 @@ | |
| return this.createFailedResult( | ||
| `Failed to parse ${join(folder, "tspconfig.yaml")}`, | ||
| "Please add tspconfig.yaml.", | ||
| RuleFailureType.ParseError, | ||
| ); | ||
| } | ||
|
|
||
|
|
@@ -68,9 +70,10 @@ | |
| } | ||
| } | ||
|
|
||
| protected createFailedResult(error: string, action: string): RuleResult { | ||
| protected createFailedResult(error: string, action: string, type: RuleFailureType): RuleResult { | ||
| return { | ||
| success: false, | ||
| type: type, | ||
| errorOutput: `- ${error}. ${action}.`, | ||
| }; | ||
| } | ||
|
|
@@ -90,12 +93,14 @@ | |
| return this.createFailedResult( | ||
| `Failed to find "parameters.${this.keyToValidate}.default" with expected value "${this.expectedValue}"`, | ||
| `Please add "parameters.${this.keyToValidate}.default" with expected value "${this.expectedValue}".`, | ||
| RuleFailureType.NotFind, | ||
| ); | ||
|
|
||
| if (!this.validateValue(parameter, this.expectedValue)) | ||
| return this.createFailedResult( | ||
| `The value of parameters.${this.keyToValidate}.default "${parameter}" does not match "${this.expectedValue}"`, | ||
| `Please update the value of "parameters.${this.keyToValidate}.default" to match "${this.expectedValue}".`, | ||
| RuleFailureType.Mismatch, | ||
| ); | ||
|
|
||
| return { success: true }; | ||
|
|
@@ -128,19 +133,106 @@ | |
| return option; | ||
| } | ||
|
|
||
| protected resolveVariables(value: string, config: any): { resolved: string; error?: string } { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here is to resolve the issue #38964. |
||
| let resolvedValue = value; | ||
| const variablePattern = /\{([^}]+)\}/g; | ||
| const maxIterations = 10; // Prevent infinite loops | ||
| let iterations = 0; | ||
|
|
||
| // Keep resolving until no more variables are found or max iterations reached | ||
| while (resolvedValue.includes("{") && iterations < maxIterations) { | ||
| iterations++; | ||
| let hasUnresolvedVariables = false; | ||
| const currentValue = resolvedValue; | ||
|
|
||
| // Reset regex lastIndex for each iteration | ||
| variablePattern.lastIndex = 0; | ||
| let match; | ||
|
|
||
| while ((match = variablePattern.exec(currentValue)) !== null) { | ||
| const variableName = match[1]; | ||
|
|
||
| // Try to resolve variable from multiple sources: | ||
| // 1. From the emitter's options (e.g., namespace, package-name) | ||
| // 2. From parameters (e.g., service-dir, output-dir) | ||
| // 3. From global options | ||
| let variableValue: string | undefined; | ||
|
|
||
| // Check emitter options first | ||
| variableValue = config?.options?.[this.emitterName]?.[variableName]; | ||
|
|
||
| // If not found, check parameters | ||
| if (!variableValue) { | ||
| variableValue = config?.parameters?.[variableName]?.default; | ||
| } | ||
|
|
||
| // If not found, check global options (for variables like output-dir) | ||
| if (!variableValue) { | ||
| variableValue = config?.[variableName]; | ||
| } | ||
|
|
||
| if (variableValue && typeof variableValue === "string") { | ||
| resolvedValue = resolvedValue.replace(`{${variableName}}`, variableValue); | ||
| } else { | ||
| hasUnresolvedVariables = true; | ||
| } | ||
| } | ||
|
|
||
| // If no progress was made in this iteration and there are still unresolved variables, return error | ||
| if (hasUnresolvedVariables && resolvedValue === currentValue) { | ||
| const unresolvedMatch = resolvedValue.match(/\{([^}]+)\}/); | ||
| const unresolvedVar = unresolvedMatch ? unresolvedMatch[1] : "unknown"; | ||
| return { | ||
| resolved: resolvedValue, | ||
| error: `Could not resolve variable {${unresolvedVar}}. The variable is not defined in options.${this.emitterName}, parameters, or config`, | ||
| }; | ||
| } | ||
|
|
||
| // If no more variables to resolve, break | ||
| if (!resolvedValue.includes("{")) { | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (iterations >= maxIterations && resolvedValue.includes("{")) { | ||
| return { | ||
| resolved: resolvedValue, | ||
| error: `Maximum resolution depth reached. Possible circular reference in variable resolution.`, | ||
| }; | ||
| } | ||
|
|
||
| return { resolved: resolvedValue }; | ||
| } | ||
|
|
||
| protected validate(config: any): RuleResult { | ||
| const option = this.tryFindOption(config); | ||
| if (option === undefined) | ||
| return this.createFailedResult( | ||
| `Failed to find "options.${this.emitterName}.${this.keyToValidate}" with expected value "${this.expectedValue}"`, | ||
| `Please add "options.${this.emitterName}.${this.keyToValidate}" with expected value "${this.expectedValue}"`, | ||
| RuleFailureType.NotFind, | ||
| ); | ||
|
|
||
| const actualValue = option as unknown as undefined | string | boolean; | ||
| let actualValue = option as unknown as undefined | string | boolean; | ||
|
|
||
| // Resolve variables if the value is a string | ||
| if (typeof actualValue === "string" && actualValue.includes("{")) { | ||
| const { resolved, error } = this.resolveVariables(actualValue, config); | ||
| if (error) { | ||
| return this.createFailedResult( | ||
| error, | ||
| `Please define the variable in your configuration or use a direct value`, | ||
| RuleFailureType.Mismatch, | ||
| ); | ||
| } | ||
| actualValue = resolved; | ||
| } | ||
|
|
||
| if (!this.validateValue(actualValue, this.expectedValue)) | ||
| return this.createFailedResult( | ||
| `The value of options.${this.emitterName}.${this.keyToValidate} "${actualValue}" does not match "${this.expectedValue}"`, | ||
| `Please update the value of "options.${this.emitterName}.${this.keyToValidate}" to match "${this.expectedValue}"`, | ||
| RuleFailureType.Mismatch, | ||
| ); | ||
|
|
||
| return { success: true }; | ||
|
|
@@ -170,13 +262,15 @@ | |
| return this.createFailedResult( | ||
| `Failed to find "options.${this.emitterName}.${this.keyToValidate}"`, | ||
| `Please add "options.${this.emitterName}.${this.keyToValidate}" with a path matching the SDK naming convention "${this.expectedValue}"`, | ||
| RuleFailureType.NotFind, | ||
| ); | ||
|
|
||
| const actualValue = option as unknown as undefined | string | boolean; | ||
| if (typeof actualValue !== "string") { | ||
| return this.createFailedResult( | ||
| `The value of options.${this.emitterName}.${this.keyToValidate} "${actualValue}" must be a string`, | ||
| `Please update the value of "options.${this.emitterName}.${this.keyToValidate}" to be a string path`, | ||
| RuleFailureType.TypeError, | ||
| ); | ||
| } | ||
|
|
||
|
|
@@ -186,6 +280,7 @@ | |
| // Format 1: {output-dir}/{service-dir}/azure-mgmt-advisor | ||
| // Format 2: {service-dir}/azure-mgmt-advisor where service-dir might include {output-dir} | ||
| // Format 3: {output-dir}/{service-dir}/azadmin/settings where we need to validate "azadmin/settings" | ||
| // Format 4: {output-dir}/sdk/dellstorage/Azure.ResourceManager.Dell.Storage - validate last part only | ||
|
|
||
| if (!actualValue.includes("/")) { | ||
| pathToValidate = actualValue; | ||
|
|
@@ -194,7 +289,20 @@ | |
| const filteredParts = pathParts.filter( | ||
| (part) => !(part === "{output-dir}" || part === "{service-dir}"), | ||
| ); | ||
| pathToValidate = filteredParts.join("/"); | ||
|
|
||
| // Strategy: Remove common directory prefixes (sdk, sdk/xxx) and validate the remaining path | ||
| // This handles: | ||
| // - "sdk/dellstorage/Azure.ResourceManager.Dell.Storage" -> "Azure.ResourceManager.Dell.Storage" | ||
| // - "azadmin/settings" -> "azadmin/settings" (no sdk prefix, keep as is) | ||
| if (filteredParts.length > 1 && filteredParts[0] === "sdk") { | ||
| // Remove "sdk" and any intermediate directory, validate only the last segment | ||
| // Example: ["sdk", "dellstorage", "Azure.ResourceManager.Dell"] -> "Azure.ResourceManager.Dell" | ||
| pathToValidate = filteredParts[filteredParts.length - 1]; | ||
| } else { | ||
| // Keep the full remaining path for validation | ||
| // Example: ["azadmin", "settings"] -> "azadmin/settings" | ||
| pathToValidate = filteredParts.join("/"); | ||
| } | ||
| } | ||
|
|
||
| // Skip validation if pathToValidate is exactly {namespace} and skipValidateNamespace is true | ||
|
|
@@ -203,27 +311,23 @@ | |
| } | ||
|
|
||
| // Resolve any variables in the pathToValidate | ||
| // Check if pathToValidate contains variables like {namespace} | ||
| const variableMatch = pathToValidate.match(/\{([^}]+)\}/); | ||
| if (variableMatch) { | ||
| const variableName = variableMatch[1]; | ||
| const variableValue = config?.options?.[this.emitterName]?.[variableName]; | ||
|
|
||
| if (variableValue && typeof variableValue === "string") { | ||
| // Replace the variable with its value | ||
| pathToValidate = pathToValidate.replace(`{${variableName}}`, variableValue); | ||
| } else { | ||
| if (pathToValidate.includes("{")) { | ||
| const { resolved, error } = this.resolveVariables(pathToValidate, config); | ||
| if (error) { | ||
| return this.createFailedResult( | ||
| `Could not resolve variable {${variableName}} in path "${pathToValidate}". The variable is not defined in options.${this.emitterName}`, | ||
| `Please define the ${variableName} variable in your configuration or use a direct path value`, | ||
| error, | ||
| `Please define the variable in your configuration or use a direct path value`, | ||
| RuleFailureType.Mismatch, | ||
| ); | ||
| } | ||
| pathToValidate = resolved; | ||
| } | ||
|
|
||
| if (!this.validateValue(pathToValidate, this.expectedValue)) | ||
| return this.createFailedResult( | ||
| `The path part "${pathToValidate}" in options.${this.emitterName}.${this.keyToValidate} does not match the required format "${this.expectedValue}"`, | ||
| `Please update the emitter-output-dir path to follow the SDK naming convention`, | ||
| RuleFailureType.Mismatch, | ||
| ); | ||
|
|
||
| return { success: true }; | ||
|
|
@@ -417,6 +521,7 @@ | |
| return this.createFailedResult( | ||
| `Neither "options.${this.emitterName}.module" nor "options.${this.emitterName}.containing-module" is defined`, | ||
| `Please add either "options.${this.emitterName}.module" or "options.${this.emitterName}.containing-module" with a value matching the pattern "${this.expectedValue}"`, | ||
| RuleFailureType.NotFind, | ||
| ); | ||
| } | ||
| if (module === undefined) return { success: true }; | ||
|
|
@@ -439,6 +544,7 @@ | |
| return this.createFailedResult( | ||
| `Neither "options.${this.emitterName}.module" nor "options.${this.emitterName}.containing-module" is defined`, | ||
| `Please add either "options.${this.emitterName}.module" or "options.${this.emitterName}.containing-module" with a value matching the pattern "${this.expectedValue}"`, | ||
| RuleFailureType.NotFind, | ||
| ); | ||
| } | ||
| if (containingModule === undefined) return { success: true }; | ||
|
|
@@ -628,6 +734,45 @@ | |
| } | ||
| } | ||
|
|
||
| // new Csharp sub rules should be added above this line | ||
| export class TspConfigHttpClientCsharpAzEmitterOutputDirSubRule extends TspconfigEmitterOptionsEmitterOutputDirSubRuleBase { | ||
| constructor() { | ||
| super("@azure-typespec/http-client-csharp", "emitter-output-dir", new RegExp(/^Azure\./)); | ||
| } | ||
| } | ||
|
|
||
| export class TspConfigHttpClientCsharpAzNamespaceSubRule extends TspconfigEmitterOptionsSubRuleBase { | ||
| constructor() { | ||
| super("@azure-typespec/http-client-csharp", "namespace", new RegExp(/^Azure\./)); | ||
| } | ||
| } | ||
|
|
||
| export class TspConfigHttpClientCsharpMgmtEmitterOutputDirSubRule extends TspconfigEmitterOptionsEmitterOutputDirSubRuleBase { | ||
| constructor() { | ||
| super( | ||
| "@azure-typespec/http-client-csharp-mgmt", | ||
| "emitter-output-dir", | ||
| new RegExp(/^Azure\.ResourceManager\./), | ||
| ); | ||
| } | ||
| protected skip(_: any, folder: string) { | ||
| return skipForDataPlane(folder); | ||
| } | ||
| } | ||
|
|
||
| export class TspConfigHttpClientCsharpMgmtNamespaceSubRule extends TspconfigEmitterOptionsSubRuleBase { | ||
| constructor() { | ||
| super( | ||
| "@azure-typespec/http-client-csharp-mgmt", | ||
| "namespace", | ||
| new RegExp(/^Azure\.ResourceManager\./), | ||
| ); | ||
| } | ||
| protected skip(_: any, folder: string) { | ||
| return skipForDataPlane(folder); | ||
| } | ||
| } | ||
|
|
||
| export const defaultRules = [ | ||
| new TspConfigCommonAzServiceDirMatchPatternSubRule(), | ||
| new TspConfigJavaAzEmitterOutputDirMatchPatternSubRule(), | ||
|
|
@@ -659,6 +804,10 @@ | |
| new TspConfigCsharpMgmtNamespaceSubRule(), | ||
| new TspConfigCsharpAzEmitterOutputDirSubRule(), | ||
| new TspConfigCsharpMgmtEmitterOutputDirSubRule(), | ||
| new TspConfigHttpClientCsharpAzNamespaceSubRule(), | ||
| new TspConfigHttpClientCsharpAzEmitterOutputDirSubRule(), | ||
| new TspConfigHttpClientCsharpMgmtNamespaceSubRule(), | ||
| new TspConfigHttpClientCsharpMgmtEmitterOutputDirSubRule(), | ||
| ]; | ||
|
|
||
| export class SdkTspConfigValidationRule implements Rule { | ||
|
|
@@ -699,7 +848,21 @@ | |
| const emitterName = emitterOptionSubRule.getEmitterName(); | ||
| if (emitterName === "@azure-tools/typespec-csharp" && isSubRuleSuccess === false) { | ||
| console.warn( | ||
| `Validation on option "${emitterOptionSubRule.getPathOfKeyToValidate()}" in "${emitterName}" are failed. However, per ${emitterName}’s decision, we will treat it as passed, please refer to https://eng.ms/docs/products/azure-developer-experience/onboard/request-exception`, | ||
| `Validation on option "${emitterOptionSubRule.getPathOfKeyToValidate()}" in "${emitterName}" are failed. However, per ${emitterName}'s decision, we will treat it as passed, please refer to https://eng.ms/docs/products/azure-developer-experience/onboard/request-exception`, | ||
| ); | ||
| isSubRuleSuccess = true; | ||
| } | ||
|
|
||
| // For @azure-typespec/http-client-csharp and @azure-typespec/http-client-csharp-mgmt, | ||
| // only ignore validation when the option is not found (missing configuration) | ||
| if ( | ||
| (emitterName === "@azure-typespec/http-client-csharp" || | ||
| emitterName === "@azure-typespec/http-client-csharp-mgmt") && | ||
| isSubRuleSuccess === false && | ||
| result.type === RuleFailureType.NotFind | ||
| ) { | ||
| console.warn( | ||
| `Validation on option "${emitterOptionSubRule.getPathOfKeyToValidate()}" in "${emitterName}" is skipped because the option is not configured.`, | ||
| ); | ||
| isSubRuleSuccess = true; | ||
| } | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.