Skip to content

Commit 541e553

Browse files
Specific diagnostic suggestions for unexpected keyword or identifier (microsoft#43005)
Error message improvement for unexpected tokens in the following situations: * A word was parsed that seems to have a low edit distance from a known common keyword * A word was parsed that seems to be a known common keyword and a name _without_ a space in-between * Parsing in a particular type of node (mostly a class property declaration) got a different word or token than expected ___ * Specific diagnostic suggestions for unexpected keywords or identifier * Don't reach into there, that's not allowed * Improved error when there is already an initializer * Specific module error message for invalid template literal strings * Skip 'unexpected keyword or identifier' diagnostics for declare nodes * Improve error for function calls in type positions * Switch class properties to old diagnostic * Corrected errors in class members and reused existing textToKeywordObj map * Corrected more baselines from the merge * Update src/compiler/parser.ts Co-authored-by: Daniel Rosenwasser <[email protected]> * Mostly addressed feedback * Clarified function call type message * Split up and clarified parsing vs error functions * Swap interface name complaints back, and skip new errors on unknown (invalid) tokens * Used tokenToString, not a raw semicolon * Inline getExpressionText helper * Remove remarks in src/compiler/parser.ts Co-authored-by: Daniel Rosenwasser <[email protected]>
1 parent 3358f13 commit 541e553

File tree

46 files changed

+1054
-304
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1054
-304
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,6 +1368,46 @@
13681368
"category": "Error",
13691369
"code": 1433
13701370
},
1371+
"Unexpected keyword or identifier.": {
1372+
"category": "Error",
1373+
"code": 1434
1374+
},
1375+
"Unknown keyword or identifier. Did you mean '{0}'?": {
1376+
"category": "Error",
1377+
"code": 1435
1378+
},
1379+
"Decorators must precede the name and all keywords of property declarations.": {
1380+
"category": "Error",
1381+
"code": 1436
1382+
},
1383+
"Namespace must be given a name.": {
1384+
"category": "Error",
1385+
"code": 1437
1386+
},
1387+
"Interface must be given a name.": {
1388+
"category": "Error",
1389+
"code": 1438
1390+
},
1391+
"Type alias must be given a name.": {
1392+
"category": "Error",
1393+
"code": 1439
1394+
},
1395+
"Variable declaration not allowed at this location.": {
1396+
"category": "Error",
1397+
"code": 1440
1398+
},
1399+
"Cannot start a function call in a type annotation.": {
1400+
"category": "Error",
1401+
"code": 1441
1402+
},
1403+
"Missing '=' before default property value.": {
1404+
"category": "Error",
1405+
"code": 1442
1406+
},
1407+
"Module declaration names may only use ' or \" quoted strings.": {
1408+
"category": "Error",
1409+
"code": 1443
1410+
},
13711411

13721412
"The types of '{0}' are incompatible between these types.": {
13731413
"category": "Error",
@@ -3356,6 +3396,10 @@
33563396
"category": "Error",
33573397
"code": 2818
33583398
},
3399+
"Namespace name cannot be '{0}'.": {
3400+
"category": "Error",
3401+
"code": 2819
3402+
},
33593403

33603404
"Import declaration '{0}' is using private name '{1}'.": {
33613405
"category": "Error",

src/compiler/parser.ts

Lines changed: 163 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,6 +1549,149 @@ namespace ts {
15491549
return false;
15501550
}
15511551

1552+
const viableKeywordSuggestions = Object.keys(textToKeywordObj).filter(keyword => keyword.length > 2);
1553+
1554+
/**
1555+
* Provides a better error message than the generic "';' expected" if possible for
1556+
* known common variants of a missing semicolon, such as from a mispelled names.
1557+
*
1558+
* @param node Node preceding the expected semicolon location.
1559+
*/
1560+
function parseErrorForMissingSemicolonAfter(node: Expression | PropertyName): void {
1561+
// Tagged template literals are sometimes used in places where only simple strings are allowed, i.e.:
1562+
// module `M1` {
1563+
// ^^^^^^^^^^^ This block is parsed as a template literal like module`M1`.
1564+
if (isTaggedTemplateExpression(node)) {
1565+
parseErrorAt(skipTrivia(sourceText, node.template.pos), node.template.end, Diagnostics.Module_declaration_names_may_only_use_or_quoted_strings);
1566+
return;
1567+
}
1568+
1569+
// Otherwise, if this isn't a well-known keyword-like identifier, give the generic fallback message.
1570+
const expressionText = ts.isIdentifier(node) ? idText(node) : undefined;
1571+
if (!expressionText || !isIdentifierText(expressionText, languageVersion)) {
1572+
parseErrorAtCurrentToken(Diagnostics._0_expected, tokenToString(SyntaxKind.SemicolonToken));
1573+
return;
1574+
}
1575+
1576+
const pos = skipTrivia(sourceText, node.pos);
1577+
1578+
// Some known keywords are likely signs of syntax being used improperly.
1579+
switch (expressionText) {
1580+
case "const":
1581+
case "let":
1582+
case "var":
1583+
parseErrorAt(pos, node.end, Diagnostics.Variable_declaration_not_allowed_at_this_location);
1584+
return;
1585+
1586+
case "declare":
1587+
// If a declared node failed to parse, it would have emitted a diagnostic already.
1588+
return;
1589+
1590+
case "interface":
1591+
parseErrorForInvalidName(Diagnostics.Interface_name_cannot_be_0, Diagnostics.Interface_must_be_given_a_name, SyntaxKind.OpenBraceToken);
1592+
return;
1593+
1594+
case "is":
1595+
parseErrorAt(pos, scanner.getTextPos(), Diagnostics.A_type_predicate_is_only_allowed_in_return_type_position_for_functions_and_methods);
1596+
return;
1597+
1598+
case "module":
1599+
case "namespace":
1600+
parseErrorForInvalidName(Diagnostics.Namespace_name_cannot_be_0, Diagnostics.Namespace_must_be_given_a_name, SyntaxKind.OpenBraceToken);
1601+
return;
1602+
1603+
case "type":
1604+
parseErrorForInvalidName(Diagnostics.Type_alias_name_cannot_be_0, Diagnostics.Type_alias_must_be_given_a_name, SyntaxKind.EqualsToken);
1605+
return;
1606+
}
1607+
1608+
// The user alternatively might have misspelled or forgotten to add a space after a common keyword.
1609+
const suggestion = getSpellingSuggestion(expressionText, viableKeywordSuggestions, n => n) ?? getSpaceSuggestion(expressionText);
1610+
if (suggestion) {
1611+
parseErrorAt(pos, node.end, Diagnostics.Unknown_keyword_or_identifier_Did_you_mean_0, suggestion);
1612+
return;
1613+
}
1614+
1615+
// Unknown tokens are handled with their own errors in the scanner
1616+
if (token() === SyntaxKind.Unknown) {
1617+
return;
1618+
}
1619+
1620+
// Otherwise, we know this some kind of unknown word, not just a missing expected semicolon.
1621+
parseErrorAt(pos, node.end, Diagnostics.Unexpected_keyword_or_identifier);
1622+
}
1623+
1624+
/**
1625+
* Reports a diagnostic error for the current token being an invalid name.
1626+
*
1627+
* @param blankDiagnostic Diagnostic to report for the case of the name being blank (matched tokenIfBlankName).
1628+
* @param nameDiagnostic Diagnostic to report for all other cases.
1629+
* @param tokenIfBlankName Current token if the name was invalid for being blank (not provided / skipped).
1630+
*/
1631+
function parseErrorForInvalidName(nameDiagnostic: DiagnosticMessage, blankDiagnostic: DiagnosticMessage, tokenIfBlankName: SyntaxKind) {
1632+
if (token() === tokenIfBlankName) {
1633+
parseErrorAtCurrentToken(blankDiagnostic);
1634+
}
1635+
else {
1636+
parseErrorAtCurrentToken(nameDiagnostic, tokenToString(token()));
1637+
}
1638+
}
1639+
1640+
function getSpaceSuggestion(expressionText: string) {
1641+
for (const keyword of viableKeywordSuggestions) {
1642+
if (expressionText.length > keyword.length + 2 && startsWith(expressionText, keyword)) {
1643+
return `${keyword} ${expressionText.slice(keyword.length)}`;
1644+
}
1645+
}
1646+
1647+
return undefined;
1648+
}
1649+
1650+
function parseSemicolonAfterPropertyName(name: PropertyName, type: TypeNode | undefined, initializer: Expression | undefined) {
1651+
switch (token()) {
1652+
case SyntaxKind.AtToken:
1653+
parseErrorAtCurrentToken(Diagnostics.Decorators_must_precede_the_name_and_all_keywords_of_property_declarations);
1654+
return;
1655+
1656+
case SyntaxKind.OpenParenToken:
1657+
parseErrorAtCurrentToken(Diagnostics.Cannot_start_a_function_call_in_a_type_annotation);
1658+
nextToken();
1659+
return;
1660+
}
1661+
1662+
if (type && !canParseSemicolon()) {
1663+
if (initializer) {
1664+
parseErrorAtCurrentToken(Diagnostics._0_expected, tokenToString(SyntaxKind.SemicolonToken));
1665+
}
1666+
else {
1667+
parseErrorAtCurrentToken(Diagnostics.Missing_before_default_property_value);
1668+
}
1669+
return;
1670+
}
1671+
1672+
if (tryParseSemicolon()) {
1673+
return;
1674+
}
1675+
1676+
// If an initializer was parsed but there is still an error in finding the next semicolon,
1677+
// we generally know there was an error already reported in the initializer...
1678+
// class Example { a = new Map([), ) }
1679+
// ~
1680+
if (initializer) {
1681+
// ...unless we've found the start of a block after a property declaration, in which
1682+
// case we can know that regardless of the initializer we should complain on the block.
1683+
// class Example { a = 0 {} }
1684+
// ~
1685+
if (token() === SyntaxKind.OpenBraceToken) {
1686+
parseErrorAtCurrentToken(Diagnostics._0_expected, tokenToString(SyntaxKind.SemicolonToken));
1687+
}
1688+
1689+
return;
1690+
}
1691+
1692+
parseErrorForMissingSemicolonAfter(name);
1693+
}
1694+
15521695
function parseExpectedJSDoc(kind: JSDocSyntaxKind) {
15531696
if (token() === kind) {
15541697
nextTokenJSDoc();
@@ -1618,18 +1761,21 @@ namespace ts {
16181761
return token() === SyntaxKind.CloseBraceToken || token() === SyntaxKind.EndOfFileToken || scanner.hasPrecedingLineBreak();
16191762
}
16201763

1621-
function parseSemicolon(): boolean {
1622-
if (canParseSemicolon()) {
1623-
if (token() === SyntaxKind.SemicolonToken) {
1624-
// consume the semicolon if it was explicitly provided.
1625-
nextToken();
1626-
}
1627-
1628-
return true;
1764+
function tryParseSemicolon() {
1765+
if (!canParseSemicolon()) {
1766+
return false;
16291767
}
1630-
else {
1631-
return parseExpected(SyntaxKind.SemicolonToken);
1768+
1769+
if (token() === SyntaxKind.SemicolonToken) {
1770+
// consume the semicolon if it was explicitly provided.
1771+
nextToken();
16321772
}
1773+
1774+
return true;
1775+
}
1776+
1777+
function parseSemicolon(): boolean {
1778+
return tryParseSemicolon() || parseExpected(SyntaxKind.SemicolonToken);
16331779
}
16341780

16351781
function createNodeArray<T extends Node>(elements: T[], pos: number, end?: number, hasTrailingComma?: boolean): NodeArray<T> {
@@ -5888,7 +6034,9 @@ namespace ts {
58886034
identifierCount++;
58896035
expression = finishNode(factory.createIdentifier(""), getNodePos());
58906036
}
5891-
parseSemicolon();
6037+
if (!tryParseSemicolon()) {
6038+
parseErrorForMissingSemicolonAfter(expression);
6039+
}
58926040
return withJSDoc(finishNode(factory.createThrowStatement(expression), pos), hasJSDoc);
58936041
}
58946042

@@ -5951,7 +6099,9 @@ namespace ts {
59516099
node = factory.createLabeledStatement(expression, parseStatement());
59526100
}
59536101
else {
5954-
parseSemicolon();
6102+
if (!tryParseSemicolon()) {
6103+
parseErrorForMissingSemicolonAfter(expression);
6104+
}
59556105
node = factory.createExpressionStatement(expression);
59566106
if (hasParen) {
59576107
// do not parse the same jsdoc twice
@@ -6546,7 +6696,7 @@ namespace ts {
65466696
const exclamationToken = !questionToken && !scanner.hasPrecedingLineBreak() ? parseOptionalToken(SyntaxKind.ExclamationToken) : undefined;
65476697
const type = parseTypeAnnotation();
65486698
const initializer = doOutsideOfContext(NodeFlags.YieldContext | NodeFlags.AwaitContext | NodeFlags.DisallowInContext, parseInitializer);
6549-
parseSemicolon();
6699+
parseSemicolonAfterPropertyName(name, type, initializer);
65506700
const node = factory.createPropertyDeclaration(decorators, modifiers, name, questionToken || exclamationToken, type, initializer);
65516701
return withJSDoc(finishNode(node, pos), hasJSDoc);
65526702
}

src/compiler/scanner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ namespace ts {
7777
tryScan<T>(callback: () => T): T;
7878
}
7979

80-
const textToKeywordObj: MapLike<KeywordSyntaxKind> = {
80+
/** @internal */
81+
export const textToKeywordObj: MapLike<KeywordSyntaxKind> = {
8182
abstract: SyntaxKind.AbstractKeyword,
8283
any: SyntaxKind.AnyKeyword,
8384
as: SyntaxKind.AsKeyword,

tests/baselines/reference/ClassDeclaration26.errors.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
tests/cases/compiler/ClassDeclaration26.ts(2,22): error TS1005: ';' expected.
1+
tests/cases/compiler/ClassDeclaration26.ts(2,18): error TS1440: Variable declaration not allowed at this location.
22
tests/cases/compiler/ClassDeclaration26.ts(4,5): error TS1068: Unexpected token. A constructor, method, accessor, or property was expected.
33
tests/cases/compiler/ClassDeclaration26.ts(4,20): error TS1005: ',' expected.
44
tests/cases/compiler/ClassDeclaration26.ts(4,23): error TS1005: '=>' expected.
@@ -8,8 +8,8 @@ tests/cases/compiler/ClassDeclaration26.ts(5,1): error TS1128: Declaration or st
88
==== tests/cases/compiler/ClassDeclaration26.ts (5 errors) ====
99
class C {
1010
public const var export foo = 10;
11-
~~~~~~
12-
!!! error TS1005: ';' expected.
11+
~~~
12+
!!! error TS1440: Variable declaration not allowed at this location.
1313

1414
var constructor() { }
1515
~~~
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
tests/cases/compiler/anonymousModules.ts(1,1): error TS2580: Cannot find name 'module'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node`.
2-
tests/cases/compiler/anonymousModules.ts(1,8): error TS1005: ';' expected.
2+
tests/cases/compiler/anonymousModules.ts(1,8): error TS1437: Namespace must be given a name.
33
tests/cases/compiler/anonymousModules.ts(4,2): error TS2580: Cannot find name 'module'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node`.
4-
tests/cases/compiler/anonymousModules.ts(4,9): error TS1005: ';' expected.
4+
tests/cases/compiler/anonymousModules.ts(4,9): error TS1437: Namespace must be given a name.
55
tests/cases/compiler/anonymousModules.ts(10,2): error TS2580: Cannot find name 'module'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node`.
6-
tests/cases/compiler/anonymousModules.ts(10,9): error TS1005: ';' expected.
6+
tests/cases/compiler/anonymousModules.ts(10,9): error TS1437: Namespace must be given a name.
77

88

99
==== tests/cases/compiler/anonymousModules.ts (6 errors) ====
1010
module {
1111
~~~~~~
1212
!!! error TS2580: Cannot find name 'module'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node`.
1313
~
14-
!!! error TS1005: ';' expected.
14+
!!! error TS1437: Namespace must be given a name.
1515
export var foo = 1;
1616

1717
module {
1818
~~~~~~
1919
!!! error TS2580: Cannot find name 'module'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node`.
2020
~
21-
!!! error TS1005: ';' expected.
21+
!!! error TS1437: Namespace must be given a name.
2222
export var bar = 1;
2323
}
2424

@@ -28,7 +28,7 @@ tests/cases/compiler/anonymousModules.ts(10,9): error TS1005: ';' expected.
2828
~~~~~~
2929
!!! error TS2580: Cannot find name 'module'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node`.
3030
~
31-
!!! error TS1005: ';' expected.
31+
!!! error TS1437: Namespace must be given a name.
3232
var x = bar;
3333
}
3434
}

tests/baselines/reference/classUpdateTests.errors.txt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ tests/cases/compiler/classUpdateTests.ts(95,1): error TS1128: Declaration or sta
1111
tests/cases/compiler/classUpdateTests.ts(99,3): error TS1128: Declaration or statement expected.
1212
tests/cases/compiler/classUpdateTests.ts(101,1): error TS1128: Declaration or statement expected.
1313
tests/cases/compiler/classUpdateTests.ts(105,3): error TS1128: Declaration or statement expected.
14-
tests/cases/compiler/classUpdateTests.ts(105,14): error TS1005: ';' expected.
14+
tests/cases/compiler/classUpdateTests.ts(105,10): error TS1434: Unexpected keyword or identifier.
15+
tests/cases/compiler/classUpdateTests.ts(105,14): error TS1068: Unexpected token. A constructor, method, accessor, or property was expected.
1516
tests/cases/compiler/classUpdateTests.ts(107,1): error TS1128: Declaration or statement expected.
1617
tests/cases/compiler/classUpdateTests.ts(111,3): error TS1128: Declaration or statement expected.
17-
tests/cases/compiler/classUpdateTests.ts(111,15): error TS1005: ';' expected.
18+
tests/cases/compiler/classUpdateTests.ts(111,11): error TS1434: Unexpected keyword or identifier.
19+
tests/cases/compiler/classUpdateTests.ts(111,15): error TS1068: Unexpected token. A constructor, method, accessor, or property was expected.
1820
tests/cases/compiler/classUpdateTests.ts(113,1): error TS1128: Declaration or statement expected.
1921

2022

21-
==== tests/cases/compiler/classUpdateTests.ts (16 errors) ====
23+
==== tests/cases/compiler/classUpdateTests.ts (18 errors) ====
2224
//
2325
// test codegen for instance properties
2426
//
@@ -154,8 +156,10 @@ tests/cases/compiler/classUpdateTests.ts(113,1): error TS1128: Declaration or st
154156
public this.p1 = 0; // ERROR
155157
~~~~~~
156158
!!! error TS1128: Declaration or statement expected.
159+
~~~~
160+
!!! error TS1434: Unexpected keyword or identifier.
157161
~
158-
!!! error TS1005: ';' expected.
162+
!!! error TS1068: Unexpected token. A constructor, method, accessor, or property was expected.
159163
}
160164
}
161165
~
@@ -166,8 +170,10 @@ tests/cases/compiler/classUpdateTests.ts(113,1): error TS1128: Declaration or st
166170
private this.p1 = 0; // ERROR
167171
~~~~~~~
168172
!!! error TS1128: Declaration or statement expected.
173+
~~~~
174+
!!! error TS1434: Unexpected keyword or identifier.
169175
~
170-
!!! error TS1005: ';' expected.
176+
!!! error TS1068: Unexpected token. A constructor, method, accessor, or property was expected.
171177
}
172178
}
173179
~

0 commit comments

Comments
 (0)