Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 47 additions & 4 deletions tsc/internal/parser/jsdoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Comment on lines +149 to +151
case ast.KindCloseParenToken, ast.KindCloseBracketToken:
if depth > 0 {
depth--
}
p.nextTokenJSDoc()
default:
p.nextTokenJSDoc()
}
}
Comment on lines +139 to +160
}

func (p *Parser) parseJSDocNameReference() *ast.Node {
pos := p.nodePos()
hasBrace := p.parseOptional(ast.KindOpenBraceToken)
Expand Down Expand Up @@ -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)) {
Comment on lines +1241 to +1242
return nil
}
}
return child
}
Expand Down
72 changes: 72 additions & 0 deletions tsc/internal/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<nil>"
}
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
}
Original file line number Diff line number Diff line change
@@ -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<keyof Sig['Args']> = ['name'];
export const b: Array<keyof Sig['Args']> = ['onSave'];
export const c: Array<keyof Sig['Args']> = ['onCancel'];

Original file line number Diff line number Diff line change
@@ -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<keyof Sig['Args']> = ['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<keyof Sig['Args']> = ['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<keyof Sig['Args']> = ['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))

Original file line number Diff line number Diff line change
@@ -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<keyof Sig['Args']> = ['name'];
>a : ("name" | "onCancel" | "onSave")[]
>['name'] : "name"[]
>'name' : "name"

export const b: Array<keyof Sig['Args']> = ['onSave'];
>b : ("name" | "onCancel" | "onSave")[]
>['onSave'] : "onSave"[]
>'onSave' : "onSave"

export const c: Array<keyof Sig['Args']> = ['onCancel'];
>c : ("name" | "onCancel" | "onSave")[]
>['onCancel'] : "onCancel"[]
>'onCancel' : "onCancel"

Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
*/
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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):
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading