From 945df46dcdab01ed0cbbb0147bcad228d9d1aff4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:03:50 +0000 Subject: [PATCH] Validate legal values for known enum/boolean global test directives Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- tsc/internal/testrunner/test_case_parser.go | 60 ++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/tsc/internal/testrunner/test_case_parser.go b/tsc/internal/testrunner/test_case_parser.go index 02cc0deae91..c1ef1a5b797 100644 --- a/tsc/internal/testrunner/test_case_parser.go +++ b/tsc/internal/testrunner/test_case_parser.go @@ -50,12 +50,13 @@ var fourslashDirectives = []string{"emitthisfile", "noopen"} // Given a test file containing // @FileName directives, // return an array of named units of code to be added to an existing compiler instance. func makeUnitsFromTest(code string, fileName string) testCaseContent { - testUnits, symlinks, currentDirectory, globalOptions, _ := ParseTestFilesAndSymlinks( + testUnits, symlinks, currentDirectory, globalOptions, _ := ParseTestFilesAndSymlinksWithOptions( code, fileName, func(filename string, content string, fileOptions map[string]string) (*testUnit, error) { return &testUnit{content: content, name: filename}, nil }, + ParseTestFilesOptions{ValidateOptionValues: true}, ) if currentDirectory == "" { @@ -123,6 +124,13 @@ type ParseTestFilesOptions struct { // In this case, an implicit first file is created using the fileName parameter. // This matches the behavior of the TypeScript fourslash test harness. AllowImplicitFirstFile bool + // If true, global directives that name a known enum or boolean compiler option + // (e.g. `@target`, `@module`) are checked against that option's legal values, and + // an illegal value (e.g. a typo like `@module: commojs`) causes a panic. This is + // only enabled for compiler/transpile test parsing; it is not used by consumers + // that intentionally parse arbitrary or legacy content, such as the fourslash test + // parser or the parser fuzzer. + ValidateOptionValues bool } // Given a test file containing // @FileName and // @symlink directives, @@ -191,6 +199,9 @@ func ParseTestFilesAndSymlinksWithOptions[T any]( if existingValue, ok := globalOptions[metaDataName]; ok && existingValue != metaDataValue { panic(fmt.Sprintf("Duplicate global option '%s': %q conflicts with previously set value %q", metaDataName, metaDataValue, existingValue)) } + if options.ValidateOptionValues { + validateGlobalOptionValue(metaDataName, strings.TrimSuffix(metaDataValue, ";")) + } globalOptions[metaDataName] = metaDataValue } continue @@ -297,3 +308,50 @@ func parseSymlinkFromTest(line string, symlinks map[string]string) bool { symlinks[strings.TrimSpace(linkMetaData[2])] = strings.TrimSpace(linkMetaData[1]) return true } + +// validateGlobalOptionValue panics if value is not a legal value for a known enum or boolean +// compiler option (e.g. `@target: es212` or `@module: commojs`). Values that are legal but +// unsupported-as-of-implementation (e.g. `@module: amd`) are not rejected here; that is handled +// separately by the test runner. Options not recognized as compiler options (e.g. harness-only +// directives like `@noImplicitReferences`) are left to downstream harness validation. +func validateGlobalOptionValue(name string, value string) { + option := core.Find(tsoptions.OptionsDeclarations, func(o *tsoptions.CommandLineOption) bool { + return strings.EqualFold(o.Name, name) + }) + if option == nil { + return + } + + var isValid func(token string) bool + var legalValues func() []string + switch option.Kind { + case tsoptions.CommandLineOptionTypeEnum: + isValid = func(token string) bool { + _, ok := option.EnumMap().Get(strings.ToLower(token)) + return ok + } + legalValues = func() []string { return slices.Sorted(option.EnumMap().Keys()) } + case tsoptions.CommandLineOptionTypeBoolean: + isValid = func(token string) bool { + lower := strings.ToLower(token) + return lower == "true" || lower == "false" + } + legalValues = func() []string { return []string{"true", "false"} } + default: + return + } + + // Values may be a comma-separated list used to generate multiple test variations + // (e.g. `@target: es5, es2015`), and may include a `*` wildcard or `-`/`!` exclusion prefix. + // Excluded values (e.g. `-es3`) are not validated, matching the test runner's tolerance of + // exclusions that no longer correspond to a recognized value (e.g. a removed option like "es3"). + for token := range strings.SplitSeq(value, ",") { + token = strings.TrimSpace(token) + if token == "" || token == "*" || strings.HasPrefix(token, "-") || strings.HasPrefix(token, "!") { + continue + } + if !isValid(token) { + panic(fmt.Sprintf("Illegal value %q for global option '%s'; expected one of: %s", token, name, strings.Join(legalValues(), ", "))) + } + } +}