From bed4539122f2c1d432e6ba579f83580974f68fd6 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 7 Nov 2025 13:23:16 -0800 Subject: [PATCH 01/14] Fix two fuzz caught bugs --- internal/parser/parser.go | 3 +++ internal/parser/testdata/fuzz/FuzzParser/6944735deac09149 | 5 +++++ internal/parser/testdata/fuzz/FuzzParser/c5dc3279768e5a11 | 5 +++++ internal/parser/utilities.go | 4 +++- 4 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 internal/parser/testdata/fuzz/FuzzParser/6944735deac09149 create mode 100644 internal/parser/testdata/fuzz/FuzzParser/c5dc3279768e5a11 diff --git a/internal/parser/parser.go b/internal/parser/parser.go index bb59c4fc90..bdbc7b4a93 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -6397,6 +6397,9 @@ func skipNonBlanks(text string, pos int) int { } func skipTo(text string, pos int, s string) int { + if pos >= len(text) { + return -1 + } i := strings.Index(text[pos:], s) if i < 0 { return -1 diff --git a/internal/parser/testdata/fuzz/FuzzParser/6944735deac09149 b/internal/parser/testdata/fuzz/FuzzParser/6944735deac09149 new file mode 100644 index 0000000000..b4d6d2e14c --- /dev/null +++ b/internal/parser/testdata/fuzz/FuzzParser/6944735deac09149 @@ -0,0 +1,5 @@ +go test fuzz v1 +string(".ts") +string("/*/") +rune('c') +byte('\x00') diff --git a/internal/parser/testdata/fuzz/FuzzParser/c5dc3279768e5a11 b/internal/parser/testdata/fuzz/FuzzParser/c5dc3279768e5a11 new file mode 100644 index 0000000000..2bb9b36285 --- /dev/null +++ b/internal/parser/testdata/fuzz/FuzzParser/c5dc3279768e5a11 @@ -0,0 +1,5 @@ +go test fuzz v1 +string(".ts") +string("/**") +rune('c') +byte('\x00') diff --git a/internal/parser/utilities.go b/internal/parser/utilities.go index ee31ae4704..2e143a835b 100644 --- a/internal/parser/utilities.go +++ b/internal/parser/utilities.go @@ -41,7 +41,9 @@ func GetJSDocCommentRanges(f *ast.NodeFactory, commentRanges []ast.CommentRange, } // Keep if the comment starts with '/**' but not if it is '/**/' return slices.DeleteFunc(commentRanges, func(comment ast.CommentRange) bool { - return comment.End() > node.End() || text[comment.Pos()+1] != '*' || text[comment.Pos()+2] != '*' || text[comment.Pos()+3] == '/' + commentStart := comment.Pos() + commentLen := comment.End() - commentStart + return comment.End() > node.End() || commentLen < 4 || text[commentStart+1] != '*' || text[commentStart+2] != '*' || text[commentStart+3] == '/' }) } From 9e852a7ff019711ca5533e018947bd89be6403bd Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 7 Nov 2025 13:23:35 -0800 Subject: [PATCH 02/14] Fix another --- internal/parser/parser.go | 2 +- internal/parser/testdata/fuzz/FuzzParser/f6dbdaa8568c9488 | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 internal/parser/testdata/fuzz/FuzzParser/f6dbdaa8568c9488 diff --git a/internal/parser/parser.go b/internal/parser/parser.go index bdbc7b4a93..93c1a27e42 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -503,7 +503,7 @@ func (p *Parser) parseListIndex(kind ParsingContext, parseElement func(p *Parser list := make([]*ast.Node, 0, 16) for i := 0; !p.isListTerminator(kind); i++ { if p.isListElement(kind, false /*inErrorRecovery*/) { - elt := parseElement(p, i) + elt := parseElement(p, len(list)) if len(p.reparseList) > 0 { for _, e := range p.reparseList { // Propagate @typedef type alias declarations outwards to a context that permits them. diff --git a/internal/parser/testdata/fuzz/FuzzParser/f6dbdaa8568c9488 b/internal/parser/testdata/fuzz/FuzzParser/f6dbdaa8568c9488 new file mode 100644 index 0000000000..499b70c05f --- /dev/null +++ b/internal/parser/testdata/fuzz/FuzzParser/f6dbdaa8568c9488 @@ -0,0 +1,5 @@ +go test fuzz v1 +string(".ts") +string(")import A,await") +rune('c') +byte('\x00') From b0cfe3e27741f648bb987857273d102b60361940 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 7 Nov 2025 13:47:35 -0800 Subject: [PATCH 03/14] Fix another --- internal/parser/parser.go | 6 +++++- internal/parser/testdata/fuzz/FuzzParser/4c0324cd37d955ff | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 internal/parser/testdata/fuzz/FuzzParser/4c0324cd37d955ff diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 93c1a27e42..7f9e570c83 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -1029,8 +1029,12 @@ func (p *Parser) parseDeclaration() *ast.Statement { func (p *Parser) parseDeclarationWorker(pos int, hasJSDoc bool, modifiers *ast.ModifierList) *ast.Statement { switch p.token { - case ast.KindVarKeyword, ast.KindLetKeyword, ast.KindConstKeyword, ast.KindUsingKeyword, ast.KindAwaitKeyword: + case ast.KindVarKeyword, ast.KindLetKeyword, ast.KindConstKeyword, ast.KindUsingKeyword: return p.parseVariableStatement(pos, hasJSDoc, modifiers) + case ast.KindAwaitKeyword: + if p.isAwaitUsingDeclaration() { + return p.parseVariableStatement(pos, hasJSDoc, modifiers) + } case ast.KindFunctionKeyword: return p.parseFunctionDeclaration(pos, hasJSDoc, modifiers) case ast.KindClassKeyword: diff --git a/internal/parser/testdata/fuzz/FuzzParser/4c0324cd37d955ff b/internal/parser/testdata/fuzz/FuzzParser/4c0324cd37d955ff new file mode 100644 index 0000000000..10b94f1a4c --- /dev/null +++ b/internal/parser/testdata/fuzz/FuzzParser/4c0324cd37d955ff @@ -0,0 +1,5 @@ +go test fuzz v1 +string(".ts") +string("@00await") +rune('c') +byte('\x00') From 702f7be75052d8e1caf83618cca989d1dedc5bc1 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:11:32 -0800 Subject: [PATCH 04/14] Suspect jsdoc fix --- internal/parser/jsdoc.go | 8 +++++++- internal/parser/testdata/fuzz/FuzzParser/3d59c16f3abc20e1 | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 internal/parser/testdata/fuzz/FuzzParser/3d59c16f3abc20e1 diff --git a/internal/parser/jsdoc.go b/internal/parser/jsdoc.go index db6a24f4d8..74c1e55245 100644 --- a/internal/parser/jsdoc.go +++ b/internal/parser/jsdoc.go @@ -918,8 +918,14 @@ func (p *Parser) parseTypedefTag(start int, tagName *ast.IdentifierNode, indent if childTypeTag != nil && childTypeTag.TypeExpression != nil && !isObjectOrObjectArrayTypeReference(childTypeTag.TypeExpression.Type()) { typeExpression = childTypeTag.TypeExpression } else { - typeExpression = p.finishNode(jsdocTypeLiteral, jsdocPropertyTags[0].Pos()) + // TODO(jakebailey) This differs from Strada but prevents a crash + pos := start + if len(jsdocPropertyTags) > 0 { + pos = jsdocPropertyTags[0].Pos() + } + typeExpression = p.finishNode(jsdocTypeLiteral, pos) } + end = typeExpression.End() } } diff --git a/internal/parser/testdata/fuzz/FuzzParser/3d59c16f3abc20e1 b/internal/parser/testdata/fuzz/FuzzParser/3d59c16f3abc20e1 new file mode 100644 index 0000000000..5b7735a2cb --- /dev/null +++ b/internal/parser/testdata/fuzz/FuzzParser/3d59c16f3abc20e1 @@ -0,0 +1,5 @@ +go test fuzz v1 +string(".ts") +string("/**@typedef @type object00") +rune('\x04') +byte('\x00') From 2e1e2fded568727daa56abd0c8e4cf6938516120 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:32:46 -0800 Subject: [PATCH 05/14] _test package --- internal/parser/parser_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 529d257949..172126a6f1 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -1,4 +1,4 @@ -package parser +package parser_test import ( "io/fs" @@ -10,6 +10,7 @@ import ( "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/parser" "github.com/microsoft/typescript-go/internal/repo" "github.com/microsoft/typescript-go/internal/testutil/fixtures" "github.com/microsoft/typescript-go/internal/tspath" @@ -46,7 +47,7 @@ func BenchmarkParse(b *testing.B) { } for b.Loop() { - ParseSourceFile(opts, sourceText, scriptKind) + parser.ParseSourceFile(opts, sourceText, scriptKind) } }) } @@ -140,6 +141,6 @@ func FuzzParser(f *testing.F) { JSDocParsingMode: jsdocParsingMode, } - ParseSourceFile(opts, sourceText, core.GetScriptKindFromFileName(fileName)) + parser.ParseSourceFile(opts, sourceText, core.GetScriptKindFromFileName(fileName)) }) } From 9297f718c6a7ce11ec78d1bb5d85cd4a903e8a16 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:37:27 -0800 Subject: [PATCH 06/14] Extract test cases --- internal/parser/parser_test.go | 88 +++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 172126a6f1..32d3e8641a 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -5,6 +5,8 @@ import ( "iter" "os" "path/filepath" + "regexp" + "strings" "testing" "github.com/microsoft/typescript-go/internal/ast" @@ -95,7 +97,6 @@ func FuzzParser(f *testing.F) { "src", "scripts", "Herebyfile.mjs", - // "tests/cases", } var extensions collections.Set[string] @@ -116,6 +117,37 @@ func FuzzParser(f *testing.F) { } } + testDirs := []string{ + filepath.Join(repo.TypeScriptSubmodulePath, "tests/cases/compiler"), + filepath.Join(repo.TypeScriptSubmodulePath, "tests/cases/conformance"), + filepath.Join(repo.TestDataPath, "tests/cases/compiler"), + } + + for _, testDir := range testDirs { + if _, err := os.Stat(testDir); os.IsNotExist(err) { + continue + } + + for file := range allParsableFiles(f, testDir) { + sourceText, err := os.ReadFile(file.path) + assert.NilError(f, err) + + testUnits, _, _, _, err := parseTestFilesAndSymlinks( + string(sourceText), + file.path, + ) + assert.NilError(f, err) + + for _, unit := range testUnits { + extension := tspath.TryGetExtensionFromPath(unit.name) + if extension == "" { + continue + } + f.Add(extension, unit.content, int32(core.ScriptTargetESNext), uint8(ast.JSDocParsingModeParseAll)) + } + } + } + f.Fuzz(func(t *testing.T, extension string, sourceText string, scriptTarget_ int32, jsdocParsingMode_ uint8) { scriptTarget := core.ScriptTarget(scriptTarget_) jsdocParsingMode := ast.JSDocParsingMode(jsdocParsingMode_) @@ -144,3 +176,57 @@ func FuzzParser(f *testing.F) { parser.ParseSourceFile(opts, sourceText, core.GetScriptKindFromFileName(fileName)) }) } + +type testUnit struct { + content string + name string +} + +// parseTestFilesAndSymlinks is a simplified version of testrunner.ParseTestFilesAndSymlinks +// that extracts individual file units from compiler test files. +func parseTestFilesAndSymlinks( + code string, + fileName string, +) (units []testUnit, symlinks map[string]string, currentDir string, globalOptions map[string]string, e error) { + var testUnits []testUnit + var currentFileContent strings.Builder + var currentFileName string + symlinks = make(map[string]string) + globalOptions = make(map[string]string) + + // Regex for parsing @filename directive + filenameRegex := regexp.MustCompile(`(?m)^\/\/\s*@filename\s*:\s*([^\r\n]*)`) + + lines := strings.Split(code, "\n") + for _, line := range lines { + if match := filenameRegex.FindStringSubmatch(line); match != nil { + // New file directive + if currentFileName != "" { + // Save previous file + testUnits = append(testUnits, testUnit{ + content: currentFileContent.String(), + name: currentFileName, + }) + currentFileContent.Reset() + } + currentFileName = strings.TrimSpace(match[1]) + } else { + // Content line + if currentFileContent.Len() != 0 { + currentFileContent.WriteRune('\n') + } + currentFileContent.WriteString(line) + } + } + + // Handle single-file case or save last file + if currentFileName == "" { + currentFileName = filepath.Base(fileName) + } + testUnits = append(testUnits, testUnit{ + content: currentFileContent.String(), + name: currentFileName, + }) + + return testUnits, symlinks, currentDir, globalOptions, nil +} From f296668cad286fc82a10ca3038d73bad8de4e284 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:43:50 -0800 Subject: [PATCH 07/14] Update fuzz tests --- internal/parser/parser_test.go | 14 +++----------- .../testdata/fuzz/FuzzParser/02b74efe61495c2a | 5 ++--- .../testdata/fuzz/FuzzParser/3d59c16f3abc20e1 | 1 - .../testdata/fuzz/FuzzParser/4c0324cd37d955ff | 1 - .../testdata/fuzz/FuzzParser/6944735deac09149 | 1 - .../testdata/fuzz/FuzzParser/9ce2d994c65c7bfe | 5 ++--- .../testdata/fuzz/FuzzParser/c5dc3279768e5a11 | 1 - .../testdata/fuzz/FuzzParser/f6dbdaa8568c9488 | 1 - 8 files changed, 7 insertions(+), 22 deletions(-) diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 32d3e8641a..548c92dde9 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -113,7 +113,7 @@ func FuzzParser(f *testing.F) { sourceText, err := os.ReadFile(file.path) assert.NilError(f, err) extension := tspath.TryGetExtensionFromPath(file.path) - f.Add(extension, string(sourceText), int32(core.ScriptTargetESNext), uint8(ast.JSDocParsingModeParseAll)) + f.Add(extension, string(sourceText), uint8(ast.JSDocParsingModeParseAll)) } } @@ -140,26 +140,18 @@ func FuzzParser(f *testing.F) { for _, unit := range testUnits { extension := tspath.TryGetExtensionFromPath(unit.name) - if extension == "" { - continue - } - f.Add(extension, unit.content, int32(core.ScriptTargetESNext), uint8(ast.JSDocParsingModeParseAll)) + f.Add(extension, unit.content, uint8(ast.JSDocParsingModeParseAll)) } } } - f.Fuzz(func(t *testing.T, extension string, sourceText string, scriptTarget_ int32, jsdocParsingMode_ uint8) { - scriptTarget := core.ScriptTarget(scriptTarget_) + f.Fuzz(func(t *testing.T, extension string, sourceText string, jsdocParsingMode_ uint8) { jsdocParsingMode := ast.JSDocParsingMode(jsdocParsingMode_) if !extensions.Has(extension) { t.Skip() } - if scriptTarget < core.ScriptTargetNone || scriptTarget > core.ScriptTargetLatest { - t.Skip() - } - if jsdocParsingMode < ast.JSDocParsingModeParseAll || jsdocParsingMode > ast.JSDocParsingModeParseNone { t.Skip() } diff --git a/internal/parser/testdata/fuzz/FuzzParser/02b74efe61495c2a b/internal/parser/testdata/fuzz/FuzzParser/02b74efe61495c2a index 6bd4a7bcdb..b39d22b6a9 100644 --- a/internal/parser/testdata/fuzz/FuzzParser/02b74efe61495c2a +++ b/internal/parser/testdata/fuzz/FuzzParser/02b74efe61495c2a @@ -1,5 +1,4 @@ go test fuzz v1 string(".ts") -string("/**@0\n * */0") -int32(99) -uint8(0) +string("") +byte('\x00') diff --git a/internal/parser/testdata/fuzz/FuzzParser/3d59c16f3abc20e1 b/internal/parser/testdata/fuzz/FuzzParser/3d59c16f3abc20e1 index 5b7735a2cb..53582d065e 100644 --- a/internal/parser/testdata/fuzz/FuzzParser/3d59c16f3abc20e1 +++ b/internal/parser/testdata/fuzz/FuzzParser/3d59c16f3abc20e1 @@ -1,5 +1,4 @@ go test fuzz v1 string(".ts") string("/**@typedef @type object00") -rune('\x04') byte('\x00') diff --git a/internal/parser/testdata/fuzz/FuzzParser/4c0324cd37d955ff b/internal/parser/testdata/fuzz/FuzzParser/4c0324cd37d955ff index 10b94f1a4c..dc85c25e4d 100644 --- a/internal/parser/testdata/fuzz/FuzzParser/4c0324cd37d955ff +++ b/internal/parser/testdata/fuzz/FuzzParser/4c0324cd37d955ff @@ -1,5 +1,4 @@ go test fuzz v1 string(".ts") string("@00await") -rune('c') byte('\x00') diff --git a/internal/parser/testdata/fuzz/FuzzParser/6944735deac09149 b/internal/parser/testdata/fuzz/FuzzParser/6944735deac09149 index b4d6d2e14c..67e2b6c60a 100644 --- a/internal/parser/testdata/fuzz/FuzzParser/6944735deac09149 +++ b/internal/parser/testdata/fuzz/FuzzParser/6944735deac09149 @@ -1,5 +1,4 @@ go test fuzz v1 string(".ts") string("/*/") -rune('c') byte('\x00') diff --git a/internal/parser/testdata/fuzz/FuzzParser/9ce2d994c65c7bfe b/internal/parser/testdata/fuzz/FuzzParser/9ce2d994c65c7bfe index 7ba034ca18..b39d22b6a9 100644 --- a/internal/parser/testdata/fuzz/FuzzParser/9ce2d994c65c7bfe +++ b/internal/parser/testdata/fuzz/FuzzParser/9ce2d994c65c7bfe @@ -1,5 +1,4 @@ go test fuzz v1 string(".ts") -string("/") -int32(99) -uint8(1) +string("") +byte('\x00') diff --git a/internal/parser/testdata/fuzz/FuzzParser/c5dc3279768e5a11 b/internal/parser/testdata/fuzz/FuzzParser/c5dc3279768e5a11 index 2bb9b36285..dcbd657548 100644 --- a/internal/parser/testdata/fuzz/FuzzParser/c5dc3279768e5a11 +++ b/internal/parser/testdata/fuzz/FuzzParser/c5dc3279768e5a11 @@ -1,5 +1,4 @@ go test fuzz v1 string(".ts") string("/**") -rune('c') byte('\x00') diff --git a/internal/parser/testdata/fuzz/FuzzParser/f6dbdaa8568c9488 b/internal/parser/testdata/fuzz/FuzzParser/f6dbdaa8568c9488 index 499b70c05f..4257792200 100644 --- a/internal/parser/testdata/fuzz/FuzzParser/f6dbdaa8568c9488 +++ b/internal/parser/testdata/fuzz/FuzzParser/f6dbdaa8568c9488 @@ -1,5 +1,4 @@ go test fuzz v1 string(".ts") string(")import A,await") -rune('c') byte('\x00') From 2c6efd71d46231169600d0c7f08c50203781cd5d Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:51:59 -0800 Subject: [PATCH 08/14] more fuzzing --- internal/ast/parseoptions.go | 14 +++++++------- internal/parser/parser_test.go | 10 +++++++--- .../testdata/fuzz/FuzzParser/02b74efe61495c2a | 2 ++ .../testdata/fuzz/FuzzParser/3d59c16f3abc20e1 | 2 ++ .../testdata/fuzz/FuzzParser/4c0324cd37d955ff | 2 ++ .../testdata/fuzz/FuzzParser/6944735deac09149 | 2 ++ .../testdata/fuzz/FuzzParser/9ce2d994c65c7bfe | 2 ++ .../testdata/fuzz/FuzzParser/c5dc3279768e5a11 | 2 ++ .../testdata/fuzz/FuzzParser/f6dbdaa8568c9488 | 2 ++ 9 files changed, 28 insertions(+), 10 deletions(-) diff --git a/internal/ast/parseoptions.go b/internal/ast/parseoptions.go index 73e8bfc6c5..a697eceb75 100644 --- a/internal/ast/parseoptions.go +++ b/internal/ast/parseoptions.go @@ -31,8 +31,8 @@ func GetSourceFileAffectingCompilerOptions(fileName string, options *core.Compil } type ExternalModuleIndicatorOptions struct { - jsx bool - force bool + JSX bool + Force bool } func GetExternalModuleIndicatorOptions(fileName string, options *core.CompilerOptions, metadata SourceFileMetaData) ExternalModuleIndicatorOptions { @@ -43,7 +43,7 @@ func GetExternalModuleIndicatorOptions(fileName string, options *core.CompilerOp switch options.GetEmitModuleDetectionKind() { case core.ModuleDetectionKindForce: // All non-declaration files are modules, declaration files still do the usual isFileProbablyExternalModule - return ExternalModuleIndicatorOptions{force: true} + return ExternalModuleIndicatorOptions{Force: true} case core.ModuleDetectionKindLegacy: // Files are modules if they have imports, exports, or import.meta return ExternalModuleIndicatorOptions{} @@ -52,8 +52,8 @@ func GetExternalModuleIndicatorOptions(fileName string, options *core.CompilerOp // If jsx is react-jsx or react-jsxdev then jsx tags force module-ness // otherwise, the presence of import or export statments (or import.meta) implies module-ness return ExternalModuleIndicatorOptions{ - jsx: options.Jsx == core.JsxEmitReactJSX || options.Jsx == core.JsxEmitReactJSXDev, - force: isFileForcedToBeModuleByFormat(fileName, options, metadata), + JSX: options.Jsx == core.JsxEmitReactJSX || options.Jsx == core.JsxEmitReactJSXDev, + Force: isFileForcedToBeModuleByFormat(fileName, options, metadata), } default: return ExternalModuleIndicatorOptions{} @@ -89,13 +89,13 @@ func getExternalModuleIndicator(file *SourceFile, opts ExternalModuleIndicatorOp return nil } - if opts.jsx { + if opts.JSX { if node := isFileModuleFromUsingJSXTag(file); node != nil { return node } } - if opts.force { + if opts.Force { return file.AsNode() } diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 548c92dde9..2c83b72d81 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -113,7 +113,7 @@ func FuzzParser(f *testing.F) { sourceText, err := os.ReadFile(file.path) assert.NilError(f, err) extension := tspath.TryGetExtensionFromPath(file.path) - f.Add(extension, string(sourceText), uint8(ast.JSDocParsingModeParseAll)) + f.Add(extension, string(sourceText), uint8(ast.JSDocParsingModeParseAll), false, false) } } @@ -140,12 +140,12 @@ func FuzzParser(f *testing.F) { for _, unit := range testUnits { extension := tspath.TryGetExtensionFromPath(unit.name) - f.Add(extension, unit.content, uint8(ast.JSDocParsingModeParseAll)) + f.Add(extension, unit.content, uint8(ast.JSDocParsingModeParseAll), false, false) } } } - f.Fuzz(func(t *testing.T, extension string, sourceText string, jsdocParsingMode_ uint8) { + f.Fuzz(func(t *testing.T, extension string, sourceText string, jsdocParsingMode_ uint8, externalModuleIndicatorOptionsJSX bool, externalModuleIndicatorOptionsForce bool) { jsdocParsingMode := ast.JSDocParsingMode(jsdocParsingMode_) if !extensions.Has(extension) { @@ -163,6 +163,10 @@ func FuzzParser(f *testing.F) { FileName: fileName, Path: path, JSDocParsingMode: jsdocParsingMode, + ExternalModuleIndicatorOptions: ast.ExternalModuleIndicatorOptions{ + JSX: externalModuleIndicatorOptionsJSX, + Force: externalModuleIndicatorOptionsForce, + }, } parser.ParseSourceFile(opts, sourceText, core.GetScriptKindFromFileName(fileName)) diff --git a/internal/parser/testdata/fuzz/FuzzParser/02b74efe61495c2a b/internal/parser/testdata/fuzz/FuzzParser/02b74efe61495c2a index b39d22b6a9..dff3eb9368 100644 --- a/internal/parser/testdata/fuzz/FuzzParser/02b74efe61495c2a +++ b/internal/parser/testdata/fuzz/FuzzParser/02b74efe61495c2a @@ -2,3 +2,5 @@ go test fuzz v1 string(".ts") string("") byte('\x00') +bool(false) +bool(false) diff --git a/internal/parser/testdata/fuzz/FuzzParser/3d59c16f3abc20e1 b/internal/parser/testdata/fuzz/FuzzParser/3d59c16f3abc20e1 index 53582d065e..7025036252 100644 --- a/internal/parser/testdata/fuzz/FuzzParser/3d59c16f3abc20e1 +++ b/internal/parser/testdata/fuzz/FuzzParser/3d59c16f3abc20e1 @@ -2,3 +2,5 @@ go test fuzz v1 string(".ts") string("/**@typedef @type object00") byte('\x00') +bool(false) +bool(false) diff --git a/internal/parser/testdata/fuzz/FuzzParser/4c0324cd37d955ff b/internal/parser/testdata/fuzz/FuzzParser/4c0324cd37d955ff index dc85c25e4d..fb1b7aa812 100644 --- a/internal/parser/testdata/fuzz/FuzzParser/4c0324cd37d955ff +++ b/internal/parser/testdata/fuzz/FuzzParser/4c0324cd37d955ff @@ -2,3 +2,5 @@ go test fuzz v1 string(".ts") string("@00await") byte('\x00') +bool(false) +bool(false) diff --git a/internal/parser/testdata/fuzz/FuzzParser/6944735deac09149 b/internal/parser/testdata/fuzz/FuzzParser/6944735deac09149 index 67e2b6c60a..c9caf93eba 100644 --- a/internal/parser/testdata/fuzz/FuzzParser/6944735deac09149 +++ b/internal/parser/testdata/fuzz/FuzzParser/6944735deac09149 @@ -2,3 +2,5 @@ go test fuzz v1 string(".ts") string("/*/") byte('\x00') +bool(false) +bool(false) diff --git a/internal/parser/testdata/fuzz/FuzzParser/9ce2d994c65c7bfe b/internal/parser/testdata/fuzz/FuzzParser/9ce2d994c65c7bfe index b39d22b6a9..dff3eb9368 100644 --- a/internal/parser/testdata/fuzz/FuzzParser/9ce2d994c65c7bfe +++ b/internal/parser/testdata/fuzz/FuzzParser/9ce2d994c65c7bfe @@ -2,3 +2,5 @@ go test fuzz v1 string(".ts") string("") byte('\x00') +bool(false) +bool(false) diff --git a/internal/parser/testdata/fuzz/FuzzParser/c5dc3279768e5a11 b/internal/parser/testdata/fuzz/FuzzParser/c5dc3279768e5a11 index dcbd657548..505e4516a4 100644 --- a/internal/parser/testdata/fuzz/FuzzParser/c5dc3279768e5a11 +++ b/internal/parser/testdata/fuzz/FuzzParser/c5dc3279768e5a11 @@ -2,3 +2,5 @@ go test fuzz v1 string(".ts") string("/**") byte('\x00') +bool(false) +bool(false) diff --git a/internal/parser/testdata/fuzz/FuzzParser/f6dbdaa8568c9488 b/internal/parser/testdata/fuzz/FuzzParser/f6dbdaa8568c9488 index 4257792200..315b16bfac 100644 --- a/internal/parser/testdata/fuzz/FuzzParser/f6dbdaa8568c9488 +++ b/internal/parser/testdata/fuzz/FuzzParser/f6dbdaa8568c9488 @@ -2,3 +2,5 @@ go test fuzz v1 string(".ts") string(")import A,await") byte('\x00') +bool(false) +bool(false) From 4acfba938281e0c5fbd2b28ee4df77a324780e87 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:15:02 -0800 Subject: [PATCH 09/14] Fix another bug --- internal/parser/reparser.go | 2 +- internal/parser/testdata/fuzz/FuzzParser/0ec0d5de7f0264d9 | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 internal/parser/testdata/fuzz/FuzzParser/0ec0d5de7f0264d9 diff --git a/internal/parser/reparser.go b/internal/parser/reparser.go index b1498ca68b..a0f3e8fc2d 100644 --- a/internal/parser/reparser.go +++ b/internal/parser/reparser.go @@ -319,7 +319,7 @@ func (p *Parser) reparseHosted(tag *ast.Node, parent *ast.Node, jsDoc *ast.Node) } } case ast.KindReturnStatement, ast.KindParenthesizedExpression: - if tag.AsJSDocTypeTag().TypeExpression != nil { + if parent.Expression() != nil && tag.AsJSDocTypeTag().TypeExpression != nil { parent.AsMutable().SetExpression(p.makeNewCast( p.factory.DeepCloneReparse(tag.AsJSDocTypeTag().TypeExpression.Type()), p.factory.DeepCloneReparse(parent.Expression()), diff --git a/internal/parser/testdata/fuzz/FuzzParser/0ec0d5de7f0264d9 b/internal/parser/testdata/fuzz/FuzzParser/0ec0d5de7f0264d9 new file mode 100644 index 0000000000..3199b97092 --- /dev/null +++ b/internal/parser/testdata/fuzz/FuzzParser/0ec0d5de7f0264d9 @@ -0,0 +1,6 @@ +go test fuzz v1 +string(".js") +string("00000000000000000000000000000000000000000000000000000000000\"00000000000000000000000\n/**@type */return") +byte('\x00') +bool(true) +bool(false) From 64123b8149a872115f0b4e46bec5394b4da799fb Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:49:49 -0800 Subject: [PATCH 10/14] Refactor reparseHosted function and add new fuzz test cases --- internal/parser/reparser.go | 11 ++++++++++- .../parser/testdata/fuzz/FuzzParser/7d4c688e1df61349 | 6 ++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 internal/parser/testdata/fuzz/FuzzParser/7d4c688e1df61349 diff --git a/internal/parser/reparser.go b/internal/parser/reparser.go index a0f3e8fc2d..9785817dfd 100644 --- a/internal/parser/reparser.go +++ b/internal/parser/reparser.go @@ -352,7 +352,7 @@ func (p *Parser) reparseHosted(tag *ast.Node, parent *ast.Node, jsDoc *ast.Node) } case ast.KindVariableDeclaration, ast.KindCommonJSExport, - ast.KindPropertyDeclaration, ast.KindPropertyAssignment, ast.KindShorthandPropertyAssignment: + ast.KindPropertyDeclaration, ast.KindPropertyAssignment: if parent.Initializer() != nil && tag.AsJSDocSatisfiesTag().TypeExpression != nil { parent.AsMutable().SetInitializer(p.makeNewCast( p.factory.DeepCloneReparse(tag.AsJSDocSatisfiesTag().TypeExpression.Type()), @@ -360,6 +360,15 @@ func (p *Parser) reparseHosted(tag *ast.Node, parent *ast.Node, jsDoc *ast.Node) false /*isAssertion*/)) p.finishMutatedNode(parent) } + case ast.KindShorthandPropertyAssignment: + shorthand := parent.AsShorthandPropertyAssignment() + if shorthand.ObjectAssignmentInitializer != nil && tag.AsJSDocSatisfiesTag().TypeExpression != nil { + shorthand.ObjectAssignmentInitializer = p.makeNewCast( + p.factory.DeepCloneReparse(tag.AsJSDocSatisfiesTag().TypeExpression.Type()), + p.factory.DeepCloneReparse(shorthand.ObjectAssignmentInitializer), + false /*isAssertion*/) + p.finishMutatedNode(parent) + } case ast.KindReturnStatement, ast.KindParenthesizedExpression, ast.KindExportAssignment, ast.KindJSExportAssignment: if parent.Expression() != nil && tag.AsJSDocSatisfiesTag().TypeExpression != nil { diff --git a/internal/parser/testdata/fuzz/FuzzParser/7d4c688e1df61349 b/internal/parser/testdata/fuzz/FuzzParser/7d4c688e1df61349 new file mode 100644 index 0000000000..8a57001ea2 --- /dev/null +++ b/internal/parser/testdata/fuzz/FuzzParser/7d4c688e1df61349 @@ -0,0 +1,6 @@ +go test fuzz v1 +string(".js") +string("0%{\n/**@satisfies */A") +byte('\x00') +bool(true) +bool(false) From 530f04a2fc1d44295055a18541156500f6e603a1 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:14:31 -0800 Subject: [PATCH 11/14] Add support for get and set accessors in JSDoc signature reparse and introduce new fuzz test case --- internal/parser/reparser.go | 4 ++++ internal/parser/testdata/fuzz/FuzzParser/f48591d3b8f41eca | 6 ++++++ 2 files changed, 10 insertions(+) create mode 100644 internal/parser/testdata/fuzz/FuzzParser/f48591d3b8f41eca diff --git a/internal/parser/reparser.go b/internal/parser/reparser.go index 9785817dfd..46e5598c50 100644 --- a/internal/parser/reparser.go +++ b/internal/parser/reparser.go @@ -134,6 +134,10 @@ func (p *Parser) reparseJSDocSignature(jsSignature *ast.Node, fun *ast.Node, jsD signature = p.factory.NewFunctionDeclaration(clonedModifiers, nil, p.factory.DeepCloneReparse(fun.Name()), nil, nil, nil, nil, nil) case ast.KindMethodDeclaration, ast.KindMethodSignature: signature = p.factory.NewMethodDeclaration(clonedModifiers, nil, p.factory.DeepCloneReparse(fun.Name()), nil, nil, nil, nil, nil, nil) + case ast.KindGetAccessor: + signature = p.factory.NewGetAccessorDeclaration(clonedModifiers, p.factory.DeepCloneReparse(fun.Name()), nil, nil, nil, nil, nil) + case ast.KindSetAccessor: + signature = p.factory.NewSetAccessorDeclaration(clonedModifiers, p.factory.DeepCloneReparse(fun.Name()), nil, nil, nil, nil, nil) case ast.KindConstructor: signature = p.factory.NewConstructorDeclaration(clonedModifiers, nil, nil, nil, nil, nil) case ast.KindJSDocCallbackTag: diff --git a/internal/parser/testdata/fuzz/FuzzParser/f48591d3b8f41eca b/internal/parser/testdata/fuzz/FuzzParser/f48591d3b8f41eca new file mode 100644 index 0000000000..14b3a7639d --- /dev/null +++ b/internal/parser/testdata/fuzz/FuzzParser/f48591d3b8f41eca @@ -0,0 +1,6 @@ +go test fuzz v1 +string(".js") +string("c(0000{\n/**@overload */get 0") +byte('\x00') +bool(false) +bool(true) From 8c6438d5eae7d5602f06fefb171984695c79cdcc Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:54:44 -0800 Subject: [PATCH 12/14] revise todo --- internal/parser/jsdoc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/parser/jsdoc.go b/internal/parser/jsdoc.go index 74c1e55245..477628c0d5 100644 --- a/internal/parser/jsdoc.go +++ b/internal/parser/jsdoc.go @@ -918,7 +918,7 @@ func (p *Parser) parseTypedefTag(start int, tagName *ast.IdentifierNode, indent if childTypeTag != nil && childTypeTag.TypeExpression != nil && !isObjectOrObjectArrayTypeReference(childTypeTag.TypeExpression.Type()) { typeExpression = childTypeTag.TypeExpression } else { - // TODO(jakebailey) This differs from Strada but prevents a crash + // !!! This differs from Strada but prevents a crash pos := start if len(jsdocPropertyTags) > 0 { pos = jsdocPropertyTags[0].Pos() From 1a20b044f1113920257288b9c597377712006d7b Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:52:48 -0800 Subject: [PATCH 13/14] lint --- internal/parser/parser_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 2c83b72d81..444806a937 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -193,8 +193,8 @@ func parseTestFilesAndSymlinks( // Regex for parsing @filename directive filenameRegex := regexp.MustCompile(`(?m)^\/\/\s*@filename\s*:\s*([^\r\n]*)`) - lines := strings.Split(code, "\n") - for _, line := range lines { + lines := strings.SplitSeq(code, "\n") + for line := range lines { if match := filenameRegex.FindStringSubmatch(line); match != nil { // New file directive if currentFileName != "" { From ab5c76f84abce73ddea0e62daff1be6235e470c1 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 7 Nov 2025 17:58:45 -0800 Subject: [PATCH 14/14] Dupe code --- internal/parser/parser_test.go | 70 +++++++--------------------------- 1 file changed, 13 insertions(+), 57 deletions(-) diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 444806a937..b2f2b9a6e4 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -5,8 +5,6 @@ import ( "iter" "os" "path/filepath" - "regexp" - "strings" "testing" "github.com/microsoft/typescript-go/internal/ast" @@ -14,6 +12,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/parser" "github.com/microsoft/typescript-go/internal/repo" + "github.com/microsoft/typescript-go/internal/testrunner" "github.com/microsoft/typescript-go/internal/testutil/fixtures" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs/osvfs" @@ -132,14 +131,25 @@ func FuzzParser(f *testing.F) { sourceText, err := os.ReadFile(file.path) assert.NilError(f, err) - testUnits, _, _, _, err := parseTestFilesAndSymlinks( + type testFile struct { + content string + name string + } + + testUnits, _, _, _, err := testrunner.ParseTestFilesAndSymlinks( string(sourceText), file.path, + func(filename string, content string, fileOptions map[string]string) (testFile, error) { + return testFile{content: content, name: filename}, nil + }, ) assert.NilError(f, err) for _, unit := range testUnits { extension := tspath.TryGetExtensionFromPath(unit.name) + if extension == "" { + continue + } f.Add(extension, unit.content, uint8(ast.JSDocParsingModeParseAll), false, false) } } @@ -172,57 +182,3 @@ func FuzzParser(f *testing.F) { parser.ParseSourceFile(opts, sourceText, core.GetScriptKindFromFileName(fileName)) }) } - -type testUnit struct { - content string - name string -} - -// parseTestFilesAndSymlinks is a simplified version of testrunner.ParseTestFilesAndSymlinks -// that extracts individual file units from compiler test files. -func parseTestFilesAndSymlinks( - code string, - fileName string, -) (units []testUnit, symlinks map[string]string, currentDir string, globalOptions map[string]string, e error) { - var testUnits []testUnit - var currentFileContent strings.Builder - var currentFileName string - symlinks = make(map[string]string) - globalOptions = make(map[string]string) - - // Regex for parsing @filename directive - filenameRegex := regexp.MustCompile(`(?m)^\/\/\s*@filename\s*:\s*([^\r\n]*)`) - - lines := strings.SplitSeq(code, "\n") - for line := range lines { - if match := filenameRegex.FindStringSubmatch(line); match != nil { - // New file directive - if currentFileName != "" { - // Save previous file - testUnits = append(testUnits, testUnit{ - content: currentFileContent.String(), - name: currentFileName, - }) - currentFileContent.Reset() - } - currentFileName = strings.TrimSpace(match[1]) - } else { - // Content line - if currentFileContent.Len() != 0 { - currentFileContent.WriteRune('\n') - } - currentFileContent.WriteString(line) - } - } - - // Handle single-file case or save last file - if currentFileName == "" { - currentFileName = filepath.Base(fileName) - } - testUnits = append(testUnits, testUnit{ - content: currentFileContent.String(), - name: currentFileName, - }) - - return testUnits, symlinks, currentDir, globalOptions, nil -}