diff --git a/tsc/internal/parser/jsdoc.go b/tsc/internal/parser/jsdoc.go index 6d239d2f228fc..5f86efcedf8fc 100644 --- a/tsc/internal/parser/jsdoc.go +++ b/tsc/internal/parser/jsdoc.go @@ -117,12 +117,49 @@ func (p *Parser) parseJSDocTypeExpression(mayOmitBraces bool) *ast.Node { t := p.parseJSDocType() p.contextFlags = saveContextFlags if hasBrace { - p.parseExpectedJSDoc(ast.KindCloseBraceToken) + p.parseJSDocTypeExpressionCloseBrace() } return p.finishNode(p.factory.NewJSDocTypeExpression(t), pos) } +// parseJSDocTypeExpressionCloseBrace consumes the closing `}` of a `{...}` JSDoc type. +// Unsupported Closure `function(...)` types parse `function` as a name and leave `(...)` +// before `}`. Skip that leftover so the tag name still parses and later nested `@property` +// tags are not dropped. Only start this recovery at `(` so unrelated missing-`}` cases +// (e.g. `@param { { (): void; } name`) still leave the name in place. +// See https://github.com/microsoft/TypeScript/issues/65020. +func (p *Parser) parseJSDocTypeExpressionCloseBrace() { + if p.parseExpectedJSDoc(ast.KindCloseBraceToken) { + return + } + if p.token != ast.KindOpenParenToken { + return + } + depth := 0 + for p.token != ast.KindAtToken && p.token != ast.KindEndOfFile { + switch p.token { + case ast.KindCloseBraceToken: + if depth == 0 { + p.nextTokenJSDoc() + return + } + depth-- + p.nextTokenJSDoc() + case ast.KindOpenBraceToken, ast.KindOpenParenToken, ast.KindOpenBracketToken: + depth++ + p.nextTokenJSDoc() + case ast.KindCloseParenToken, ast.KindCloseBracketToken: + if depth > 0 { + depth-- + } + p.nextTokenJSDoc() + default: + p.nextTokenJSDoc() + } + } +} + func (p *Parser) parseJSDocNameReference() *ast.Node { pos := p.nodePos() hasBrace := p.parseOptional(ast.KindOpenBraceToken) @@ -1196,9 +1233,15 @@ func (p *Parser) parseChildParameterOrPropertyTag(target propertyLikeParse, inde if canParseTag && p.scanner.CanFollowJSDocAt() { child := p.tryParseChildTag(target, indent) if child != nil && name != nil && - (child.Kind == ast.KindJSDocParameterTag || child.Kind == ast.KindJSDocPropertyTag) && - (ast.IsIdentifier(child.Name()) || !textsEqual(name, child.Name().AsQualifiedName().Left)) { - return nil + (child.Kind == ast.KindJSDocParameterTag || child.Kind == ast.KindJSDocPropertyTag) { + childName := child.Name() + // A missing name means this tag's type failed to parse and recovery could + // not read the name. Keep it as a nested child so subsequent matching + // nested tags are still collected rather than aborting the nested literal. + if !ast.NodeIsMissing(childName) && + (ast.IsIdentifier(childName) || !textsEqual(name, childName.AsQualifiedName().Left)) { + return nil + } } return child } diff --git a/tsc/internal/parser/parser_test.go b/tsc/internal/parser/parser_test.go index ffa96713eb444..9de7078c6e52f 100644 --- a/tsc/internal/parser/parser_test.go +++ b/tsc/internal/parser/parser_test.go @@ -571,3 +571,75 @@ namespace N { assert.Equal(t, positionMap.UTF8ToUTF16(afterBoxDrawingCharacter), afterBoxDrawingCharacter-2) assert.Equal(t, positionMap.UTF8ToUTF16(len(sourceText)), len(sourceText)-2) } + + +func TestJSDocNestedPropertyRecoveryAfterBadType(t *testing.T) { + t.Parallel() + // Regression for https://github.com/microsoft/TypeScript/issues/65020: + // a nested @property whose type does not parse must not drop later nested properties. + sourceText := `/** + * @typedef Sig + * @property {object} Args + * @property {string} Args.name + * @property {function(string)=} Args.onSave + * @property {((v: string) => void)=} Args.onCancel + */ +export {}; +` + opts := ast.SourceFileParseOptions{FileName: "/index.js", PathKey: "/index.js"} + file := parser.ParseSourceFile(opts, sourceText, core.ScriptKindJS) + var typedef *ast.Node + for _, statement := range file.Statements.Nodes { + for _, j := range statement.JSDoc(file) { + if j.AsJSDoc().Tags == nil { + continue + } + for _, tag := range j.AsJSDoc().Tags.Nodes { + if tag.Kind == ast.KindJSDocTypedefTag { + typedef = tag + } + } + } + } + assert.Assert(t, typedef != nil) + inner := typedef.TypeExpression() + if inner.Kind == ast.KindJSDocTypeExpression { + inner = inner.Type() + } + assert.Equal(t, inner.Kind, ast.KindJSDocTypeLiteral) + props := inner.AsJSDocTypeLiteral().JSDocPropertyTags + assert.Assert(t, len(props) >= 1) + args := props[0] + assert.Equal(t, entityText(args.Name()), "Args") + assert.Assert(t, args.TypeExpression() != nil && args.TypeExpression().Type() != nil) + assert.Equal(t, args.TypeExpression().Type().Kind, ast.KindJSDocTypeLiteral) + nested := args.TypeExpression().Type().AsJSDocTypeLiteral().JSDocPropertyTags + names := make([]string, 0, len(nested)) + for _, np := range nested { + names = append(names, entityText(np.Name())) + } + assert.Assert(t, containsString(names, "Args.name"), "names=%v", names) + assert.Assert(t, containsString(names, "Args.onCancel"), "later nested property must be kept; names=%v", names) +} + +func entityText(n *ast.Node) string { + if n == nil { + return "" + } + if ast.IsIdentifier(n) { + return n.Text() + } + if ast.IsQualifiedName(n) { + return entityText(n.AsQualifiedName().Left) + "." + entityText(n.AsQualifiedName().Right) + } + return n.Kind.String() +} + +func containsString(items []string, want string) bool { + for _, item := range items { + if item == want { + return true + } + } + return false +} diff --git a/tsc/testdata/baselines/reference/compiler/jsdocNestedPropertyRecoveryAfterBadType.errors.txt b/tsc/testdata/baselines/reference/compiler/jsdocNestedPropertyRecoveryAfterBadType.errors.txt new file mode 100644 index 0000000000000..df2b9564c5ab5 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/jsdocNestedPropertyRecoveryAfterBadType.errors.txt @@ -0,0 +1,23 @@ +index.js(5,23): error TS1005: '}' expected. + + +==== index.js (1 errors) ==== + /** + * @typedef Sig + * @property {object} Args + * @property {string} Args.name + * @property {function(string)=} Args.onSave + ~ +!!! error TS1005: '}' expected. + * @property {((v: string) => void)=} Args.onCancel + */ + + export {}; + +==== check.ts (0 errors) ==== + import type { Sig } from './index.js'; + + export const a: Array = ['name']; + export const b: Array = ['onSave']; + export const c: Array = ['onCancel']; + \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/jsdocNestedPropertyRecoveryAfterBadType.symbols b/tsc/testdata/baselines/reference/compiler/jsdocNestedPropertyRecoveryAfterBadType.symbols new file mode 100644 index 0000000000000..46eb4a734ba9d --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/jsdocNestedPropertyRecoveryAfterBadType.symbols @@ -0,0 +1,33 @@ +//// [tests/cases/compiler/jsdocNestedPropertyRecoveryAfterBadType.ts] //// + +=== index.js === + +/** + * @typedef Sig + * @property {object} Args + * @property {string} Args.name + * @property {function(string)=} Args.onSave + * @property {((v: string) => void)=} Args.onCancel + */ + +export {}; + +=== check.ts === +import type { Sig } from './index.js'; +>Sig : Symbol(Sig, Decl(check.ts, 0, 13)) + +export const a: Array = ['name']; +>a : Symbol(a, Decl(check.ts, 2, 12)) +>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --) ... and 4 more) +>Sig : Symbol(Sig, Decl(check.ts, 0, 13)) + +export const b: Array = ['onSave']; +>b : Symbol(b, Decl(check.ts, 3, 12)) +>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --) ... and 4 more) +>Sig : Symbol(Sig, Decl(check.ts, 0, 13)) + +export const c: Array = ['onCancel']; +>c : Symbol(c, Decl(check.ts, 4, 12)) +>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --) ... and 4 more) +>Sig : Symbol(Sig, Decl(check.ts, 0, 13)) + diff --git a/tsc/testdata/baselines/reference/compiler/jsdocNestedPropertyRecoveryAfterBadType.types b/tsc/testdata/baselines/reference/compiler/jsdocNestedPropertyRecoveryAfterBadType.types new file mode 100644 index 0000000000000..98d98ac348c33 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/jsdocNestedPropertyRecoveryAfterBadType.types @@ -0,0 +1,33 @@ +//// [tests/cases/compiler/jsdocNestedPropertyRecoveryAfterBadType.ts] //// + +=== index.js === + +/** + * @typedef Sig + * @property {object} Args + * @property {string} Args.name + * @property {function(string)=} Args.onSave + * @property {((v: string) => void)=} Args.onCancel + */ + +export {}; + +=== check.ts === +import type { Sig } from './index.js'; +>Sig : Sig + +export const a: Array = ['name']; +>a : ("name" | "onCancel" | "onSave")[] +>['name'] : "name"[] +>'name' : "name" + +export const b: Array = ['onSave']; +>b : ("name" | "onCancel" | "onSave")[] +>['onSave'] : "onSave"[] +>'onSave' : "onSave" + +export const c: Array = ['onCancel']; +>c : ("name" | "onCancel" | "onSave")[] +>['onCancel'] : "onCancel"[] +>'onCancel' : "onCancel" + diff --git a/tsc/testdata/baselines/reference/conformance/typedefTagWrapping.errors.txt b/tsc/testdata/baselines/reference/conformance/typedefTagWrapping.errors.txt index 7501b5fd05b30..160f5b15d5493 100644 --- a/tsc/testdata/baselines/reference/conformance/typedefTagWrapping.errors.txt +++ b/tsc/testdata/baselines/reference/conformance/typedefTagWrapping.errors.txt @@ -1,28 +1,13 @@ -mod1.js(2,21): error TS1003: Identifier expected. -mod1.js(2,22): error TS2300: Duplicate identifier '(Missing)'. mod1.js(2,22): error TS1005: '}' expected. -mod1.js(9,12): error TS2552: Cannot find name 'Type1'. Did you mean 'Type2'? -mod3.js(4,21): error TS1003: Identifier expected. -mod3.js(4,22): error TS2300: Duplicate identifier '(Missing)'. mod3.js(4,22): error TS1005: '}' expected. -mod3.js(10,12): error TS2304: Cannot find name 'StringOrNumber1'. -mod4.js(4,21): error TS1003: Identifier expected. -mod4.js(4,22): error TS2300: Duplicate identifier '(Missing)'. mod4.js(4,22): error TS1005: '}' expected. -mod4.js(11,12): error TS2304: Cannot find name 'StringOrNumber2'. mod7.js(5,7): error TS1110: Type expected. mod7.js(8,4): error TS1110: Type expected. -==== mod1.js (4 errors) ==== +==== mod1.js (1 errors) ==== /** * @typedef {function(string): boolean} - ~ -!!! error TS1003: Identifier expected. - -!!! error TS2300: Duplicate identifier '(Missing)'. -!!! related TS6203 mod3.js:4:22: '(Missing)' was also declared here. -!!! related TS6203 mod4.js:4:22: '(Missing)' was also declared here. ~ !!! error TS1005: '}' expected. * Type1 @@ -32,8 +17,6 @@ mod7.js(8,4): error TS1110: Type expected. * Tries to use a type whose name is on a different * line than the typedef tag. * @param {Type1} func The function to call. - ~~~~~ -!!! error TS2552: Cannot find name 'Type1'. Did you mean 'Type2'? * @param {string} arg The argument to call it with. * @returns {boolean} The return. */ @@ -59,16 +42,11 @@ mod7.js(8,4): error TS1110: Type expected. return obj.boo ? obj.num : obj.str; } -==== mod3.js (4 errors) ==== +==== mod3.js (1 errors) ==== /** * A function whose signature is very long. * * @typedef {function(boolean, string, number): - ~ -!!! error TS1003: Identifier expected. - -!!! error TS2300: Duplicate identifier '(Missing)'. -!!! related TS6203 mod1.js:2:22: '(Missing)' was also declared here. ~ !!! error TS1005: '}' expected. * (string|number)} StringOrNumber1 @@ -77,8 +55,6 @@ mod7.js(8,4): error TS1110: Type expected. /** * Makes use of a function type with a long signature. * @param {StringOrNumber1} func The function. - ~~~~~~~~~~~~~~~ -!!! error TS2304: Cannot find name 'StringOrNumber1'. * @param {boolean} bool The condition. * @param {string} str The string. * @param {number} num The number. @@ -88,16 +64,11 @@ mod7.js(8,4): error TS1110: Type expected. return func(bool, str, num) } -==== mod4.js (4 errors) ==== +==== mod4.js (1 errors) ==== /** * A function whose signature is very long. * * @typedef {function(boolean, string, - ~ -!!! error TS1003: Identifier expected. - -!!! error TS2300: Duplicate identifier '(Missing)'. -!!! related TS6203 mod1.js:2:22: '(Missing)' was also declared here. ~ !!! error TS1005: '}' expected. * number): @@ -107,8 +78,6 @@ mod7.js(8,4): error TS1110: Type expected. /** * Makes use of a function type with a long signature. * @param {StringOrNumber2} func The function. - ~~~~~~~~~~~~~~~ -!!! error TS2304: Cannot find name 'StringOrNumber2'. * @param {boolean} bool The condition. * @param {string} str The string. * @param {number} num The number. diff --git a/tsc/testdata/baselines/reference/conformance/typedefTagWrapping.types b/tsc/testdata/baselines/reference/conformance/typedefTagWrapping.types index ebd2541cad7ea..1dcd2fa9729b0 100644 --- a/tsc/testdata/baselines/reference/conformance/typedefTagWrapping.types +++ b/tsc/testdata/baselines/reference/conformance/typedefTagWrapping.types @@ -15,12 +15,12 @@ */ function callIt(func, arg) { >callIt : (func: Type1, arg: string) => boolean ->func : Type1 +>func : Function >arg : string return func(arg); >func(arg) : any ->func : Type1 +>func : Function >arg : string } @@ -73,14 +73,14 @@ function check(obj) { */ function use1(func, bool, str, num) { >use1 : (func: StringOrNumber1, bool: boolean, str: string, num: number) => string | number ->func : StringOrNumber1 +>func : Function >bool : boolean >str : string >num : number return func(bool, str, num) >func(bool, str, num) : any ->func : StringOrNumber1 +>func : Function >bool : boolean >str : string >num : number @@ -105,14 +105,14 @@ function use1(func, bool, str, num) { */ function use2(func, bool, str, num) { >use2 : (func: StringOrNumber2, bool: boolean, str: string, num: number) => string | number ->func : StringOrNumber2 +>func : Function >bool : boolean >str : string >num : number return func(bool, str, num) >func(bool, str, num) : any ->func : StringOrNumber2 +>func : Function >bool : boolean >str : string >num : number diff --git a/tsc/testdata/tests/cases/compiler/jsdocNestedPropertyRecoveryAfterBadType.ts b/tsc/testdata/tests/cases/compiler/jsdocNestedPropertyRecoveryAfterBadType.ts new file mode 100644 index 0000000000000..0694da2253d54 --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/jsdocNestedPropertyRecoveryAfterBadType.ts @@ -0,0 +1,22 @@ +// @strict: true +// @noEmit: true +// @allowJs: true +// @checkJs: true + +// @filename: index.js +/** + * @typedef Sig + * @property {object} Args + * @property {string} Args.name + * @property {function(string)=} Args.onSave + * @property {((v: string) => void)=} Args.onCancel + */ + +export {}; + +// @filename: check.ts +import type { Sig } from './index.js'; + +export const a: Array = ['name']; +export const b: Array = ['onSave']; +export const c: Array = ['onCancel'];