diff --git a/common/changes/@microsoft/rush/copilot-validate-parameter-names-to-ignore_2025-11-13-23-26.json b/common/changes/@microsoft/rush/copilot-validate-parameter-names-to-ignore_2025-11-13-23-26.json new file mode 100644 index 00000000000..68f87a1f804 --- /dev/null +++ b/common/changes/@microsoft/rush/copilot-validate-parameter-names-to-ignore_2025-11-13-23-26.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "", + "type": "none", + "packageName": "@microsoft/rush" + } + ], + "packageName": "@microsoft/rush", + "email": "198982749+Copilot@users.noreply.github.com" +} diff --git a/libraries/rush-lib/src/api/RushProjectConfiguration.ts b/libraries/rush-lib/src/api/RushProjectConfiguration.ts index 6ee20c8e91a..7dd28c3c789 100644 --- a/libraries/rush-lib/src/api/RushProjectConfiguration.ts +++ b/libraries/rush-lib/src/api/RushProjectConfiguration.ts @@ -341,6 +341,35 @@ export class RushProjectConfiguration { } } } + + // Validate that parameter names to ignore actually exist for this operation + if (operationSettings.parameterNamesToIgnore) { + // Build a set of valid parameter names for this phase + const validParameterNames: Set = new Set(); + for (const parameter of phase.associatedParameters) { + validParameterNames.add(parameter.longName); + } + + // Collect all invalid parameter names + const invalidParameterNames: string[] = []; + for (const parameterName of operationSettings.parameterNamesToIgnore) { + if (!validParameterNames.has(parameterName)) { + invalidParameterNames.push(parameterName); + } + } + + // Report all invalid parameters in a single message + if (invalidParameterNames.length > 0) { + terminal.writeErrorLine( + `The project "${project.packageName}" has a ` + + `"${RUSH_PROJECT_CONFIGURATION_FILE.projectRelativeFilePath}" configuration that specifies ` + + `invalid parameter(s) in "parameterNamesToIgnore" for operation "${operationName}": ` + + `${invalidParameterNames.join(', ')}. ` + + `Valid parameters for this operation are: ${Array.from(validParameterNames).sort().join(', ') || '(none)'}.` + ); + hasErrors = true; + } + } } } diff --git a/libraries/rush-lib/src/api/test/RushProjectConfiguration.test.ts b/libraries/rush-lib/src/api/test/RushProjectConfiguration.test.ts index e4bbc4971fd..a1352f49fa1 100644 --- a/libraries/rush-lib/src/api/test/RushProjectConfiguration.test.ts +++ b/libraries/rush-lib/src/api/test/RushProjectConfiguration.test.ts @@ -2,6 +2,7 @@ // See LICENSE in the project root for license information. import { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal'; +import type { CommandLineParameter } from '@rushstack/ts-command-line'; import type { IPhase } from '../CommandLineConfiguration'; import type { RushConfigurationProject } from '../RushConfigurationProject'; @@ -57,7 +58,36 @@ function validateConfiguration(rushProjectConfiguration: RushProjectConfiguratio try { rushProjectConfiguration.validatePhaseConfiguration( Array.from(rushProjectConfiguration.operationSettingsByOperationName.keys()).map( - (phaseName) => ({ name: phaseName }) as IPhase + (phaseName) => ({ name: phaseName, associatedParameters: new Set() }) as IPhase + ), + terminal + ); + } finally { + expect(terminalProvider.getOutput()).toMatchSnapshot('validation: terminal output'); + expect(terminalProvider.getErrorOutput()).toMatchSnapshot('validation: terminal error'); + expect(terminalProvider.getWarningOutput()).toMatchSnapshot('validation: terminal warning'); + expect(terminalProvider.getVerboseOutput()).toMatchSnapshot('validation: terminal verbose'); + } + } +} + +function validateConfigurationWithParameters( + rushProjectConfiguration: RushProjectConfiguration | undefined, + parameterNames: string[] +): void { + const terminalProvider: StringBufferTerminalProvider = new StringBufferTerminalProvider(); + const terminal: Terminal = new Terminal(terminalProvider); + + if (rushProjectConfiguration) { + try { + // Create mock parameters with the specified names + const mockParameters = new Set( + parameterNames.map((name) => ({ longName: name }) as CommandLineParameter) + ); + + rushProjectConfiguration.validatePhaseConfiguration( + Array.from(rushProjectConfiguration.operationSettingsByOperationName.keys(), + (phaseName) => ({ name: phaseName, associatedParameters: mockParameters }) as IPhase ), terminal ); @@ -100,6 +130,33 @@ describe(RushProjectConfiguration.name, () => { expect(() => validateConfiguration(rushProjectConfiguration)).toThrowError(); }); + + it('validates that parameters in parameterNamesToIgnore exist for the operation', async () => { + const rushProjectConfiguration: RushProjectConfiguration | undefined = + await loadProjectConfigurationAsync('test-project-e'); + + expect(() => validateConfiguration(rushProjectConfiguration)).toThrowError(); + }); + + it('validates nonexistent parameters when operation has valid parameters', async () => { + const rushProjectConfiguration: RushProjectConfiguration | undefined = + await loadProjectConfigurationAsync('test-project-f'); + + // Provide some valid parameters for the operation + expect(() => + validateConfigurationWithParameters(rushProjectConfiguration, ['--production', '--verbose']) + ).toThrowError(); + }); + + it('validates mix of existent and nonexistent parameters', async () => { + const rushProjectConfiguration: RushProjectConfiguration | undefined = + await loadProjectConfigurationAsync('test-project-g'); + + // Provide some valid parameters, test-project-g references both valid and invalid ones + expect(() => + validateConfigurationWithParameters(rushProjectConfiguration, ['--production', '--verbose']) + ).toThrowError(); + }); }); describe(RushProjectConfiguration.prototype.getCacheDisabledReason.name, () => { diff --git a/libraries/rush-lib/src/api/test/__snapshots__/RushProjectConfiguration.test.ts.snap b/libraries/rush-lib/src/api/test/__snapshots__/RushProjectConfiguration.test.ts.snap index a5ff1832598..2ebe9d5908a 100644 --- a/libraries/rush-lib/src/api/test/__snapshots__/RushProjectConfiguration.test.ts.snap +++ b/libraries/rush-lib/src/api/test/__snapshots__/RushProjectConfiguration.test.ts.snap @@ -71,3 +71,27 @@ exports[`RushProjectConfiguration operationSettingsByOperationName loads a rush- exports[`RushProjectConfiguration operationSettingsByOperationName loads a rush-project.json config that extends another config file: validation: terminal warning 1`] = `""`; exports[`RushProjectConfiguration operationSettingsByOperationName throws an error when loading a rush-project.json config that lists an operation twice 1`] = `"The operation \\"_phase:a\\" occurs multiple times in the \\"operationSettings\\" array in \\"/config/rush-project.json\\"."`; + +exports[`RushProjectConfiguration operationSettingsByOperationName validates mix of existent and nonexistent parameters: validation: terminal error 1`] = `"The project \\"test-project-g\\" has a \\"config/rush-project.json\\" configuration that specifies invalid parameter(s) in \\"parameterNamesToIgnore\\" for operation \\"_phase:build\\": --nonexistent-param. Valid parameters for this operation are: --production, --verbose.[n]"`; + +exports[`RushProjectConfiguration operationSettingsByOperationName validates mix of existent and nonexistent parameters: validation: terminal output 1`] = `""`; + +exports[`RushProjectConfiguration operationSettingsByOperationName validates mix of existent and nonexistent parameters: validation: terminal verbose 1`] = `""`; + +exports[`RushProjectConfiguration operationSettingsByOperationName validates mix of existent and nonexistent parameters: validation: terminal warning 1`] = `""`; + +exports[`RushProjectConfiguration operationSettingsByOperationName validates nonexistent parameters when operation has valid parameters: validation: terminal error 1`] = `"The project \\"test-project-f\\" has a \\"config/rush-project.json\\" configuration that specifies invalid parameter(s) in \\"parameterNamesToIgnore\\" for operation \\"_phase:build\\": --nonexistent-param, --another-nonexistent. Valid parameters for this operation are: --production, --verbose.[n]"`; + +exports[`RushProjectConfiguration operationSettingsByOperationName validates nonexistent parameters when operation has valid parameters: validation: terminal output 1`] = `""`; + +exports[`RushProjectConfiguration operationSettingsByOperationName validates nonexistent parameters when operation has valid parameters: validation: terminal verbose 1`] = `""`; + +exports[`RushProjectConfiguration operationSettingsByOperationName validates nonexistent parameters when operation has valid parameters: validation: terminal warning 1`] = `""`; + +exports[`RushProjectConfiguration operationSettingsByOperationName validates that parameters in parameterNamesToIgnore exist for the operation: validation: terminal error 1`] = `"The project \\"test-project-e\\" has a \\"config/rush-project.json\\" configuration that specifies invalid parameter(s) in \\"parameterNamesToIgnore\\" for operation \\"_phase:build\\": --invalid-parameter, --another-invalid, -malformed-parameter. Valid parameters for this operation are: (none).[n]"`; + +exports[`RushProjectConfiguration operationSettingsByOperationName validates that parameters in parameterNamesToIgnore exist for the operation: validation: terminal output 1`] = `""`; + +exports[`RushProjectConfiguration operationSettingsByOperationName validates that parameters in parameterNamesToIgnore exist for the operation: validation: terminal verbose 1`] = `""`; + +exports[`RushProjectConfiguration operationSettingsByOperationName validates that parameters in parameterNamesToIgnore exist for the operation: validation: terminal warning 1`] = `""`; diff --git a/libraries/rush-lib/src/api/test/jsonFiles/test-project-e/config/rush-project.json b/libraries/rush-lib/src/api/test/jsonFiles/test-project-e/config/rush-project.json new file mode 100644 index 00000000000..0dfe8b808e0 --- /dev/null +++ b/libraries/rush-lib/src/api/test/jsonFiles/test-project-e/config/rush-project.json @@ -0,0 +1,9 @@ +{ + "operationSettings": [ + { + "operationName": "_phase:build", + "outputFolderNames": ["lib"], + "parameterNamesToIgnore": ["--invalid-parameter", "--another-invalid", "-malformed-parameter"] + } + ] +} diff --git a/libraries/rush-lib/src/api/test/jsonFiles/test-project-f/config/rush-project.json b/libraries/rush-lib/src/api/test/jsonFiles/test-project-f/config/rush-project.json new file mode 100644 index 00000000000..fe9cc4909ae --- /dev/null +++ b/libraries/rush-lib/src/api/test/jsonFiles/test-project-f/config/rush-project.json @@ -0,0 +1,9 @@ +{ + "operationSettings": [ + { + "operationName": "_phase:build", + "outputFolderNames": ["lib"], + "parameterNamesToIgnore": ["--nonexistent-param", "--another-nonexistent"] + } + ] +} diff --git a/libraries/rush-lib/src/api/test/jsonFiles/test-project-g/config/rush-project.json b/libraries/rush-lib/src/api/test/jsonFiles/test-project-g/config/rush-project.json new file mode 100644 index 00000000000..ab34f9ea349 --- /dev/null +++ b/libraries/rush-lib/src/api/test/jsonFiles/test-project-g/config/rush-project.json @@ -0,0 +1,9 @@ +{ + "operationSettings": [ + { + "operationName": "_phase:build", + "outputFolderNames": ["lib"], + "parameterNamesToIgnore": ["--production", "--nonexistent-param", "--verbose"] + } + ] +}