β¬ οΈ Back to Table of Contents
π check-syntax-errors¶
π Analysis Summary¶
| Metric | Count |
|---|---|
| π§ Functions | 3 |
| π¦ Imports | 10 |
| π Variables & Constants | 1 |
π Table of Contents¶
π οΈ File Location:¶
π packages/typescript-estree/src/check-syntax-errors.ts
π¦ Imports¶
| Name | Source |
|---|---|
TSNode |
./ts-estree |
checkModifiers |
./check-modifiers |
getImportClausePhaseModifier |
./getImportClausePhaseModifier |
isValidAssignmentTarget |
./node-utils |
createError |
./node-utils |
hasModifier |
./node-utils |
getDeclarationKind |
./node-utils |
getTextForTokenKind |
./node-utils |
isEntityNameExpression |
./node-utils |
declarationNameToString |
./node-utils |
Variables & Constants¶
| Name | Type | Kind | Value | Exported |
|---|---|---|---|---|
SyntaxKind |
any |
const | ts.SyntaxKind |
β |
Functions¶
checkSyntaxError(tsNode: ts.Node, parent: TSNode, allowPattern: boolean): void¶
Parameters:
tsNodets.NodeparentTSNodeallowPatternboolean
Returns: void
Calls:
checkModifiers (from ./check-modifiers)node.caseBlock.clauses.filtercreateError (from ./node-utils)hasModifier (from ./node-utils)getDeclarationKind (from ./node-utils)['await using', 'const', 'using'].includes['let', 'var'].includesgetTextForTokenKind (from ./node-utils)isValidAssignmentTarget (from ./node-utils)getImportClausePhaseModifier (from ./getImportClausePhaseModifier)assertModuleSpecifierts.tokenToStringisEntityNameExpression (from ./node-utils)ts.isOptionalChaindeclarationNameToString (from ./node-utils)node.getSourceFilets.isStringLiteralcheckForStatementDeclaration
Internal Comments:
// Definite assignment only allowed for non-declare let and var
// eslint-disable-next-line @typescript-eslint/no-deprecated (x8)
/**
* ESTree uses UpdateExpression for ++/--
*/
// intentional fallthrough
// `implements`
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
// otherwise, it is a non-type accessor - intentional fallthrough
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Fixme: confirm if it's possible
Code
export function checkSyntaxError(
tsNode: ts.Node,
parent: TSNode,
allowPattern: boolean,
): void {
checkModifiers(tsNode);
const node = tsNode as TSNode;
switch (node.kind) {
case SyntaxKind.SwitchStatement:
if (
node.caseBlock.clauses.filter(
switchCase => switchCase.kind === SyntaxKind.DefaultClause,
).length > 1
) {
throw createError(
node,
"A 'default' clause cannot appear more than once in a 'switch' statement.",
);
}
break;
case SyntaxKind.ThrowStatement:
if (node.expression.end === node.expression.pos) {
throw createError(node, 'A throw statement must throw an expression.');
}
break;
case SyntaxKind.CatchClause:
if (node.variableDeclaration?.initializer) {
throw createError(
node.variableDeclaration.initializer,
'Catch clause variable cannot have an initializer.',
);
}
break;
case SyntaxKind.FunctionDeclaration: {
const isDeclare = hasModifier(SyntaxKind.DeclareKeyword, node);
const isAsync = hasModifier(SyntaxKind.AsyncKeyword, node);
const isGenerator = !!node.asteriskToken;
if (isDeclare) {
if (node.body) {
throw createError(
node,
'An implementation cannot be declared in ambient contexts.',
);
} else if (isAsync) {
throw createError(
node,
"'async' modifier cannot be used in an ambient context.",
);
} else if (isGenerator) {
throw createError(
node,
'Generators are not allowed in an ambient context.',
);
}
} else if (!node.body && isGenerator) {
throw createError(
node,
'A function signature cannot be declared as a generator.',
);
}
break;
}
case SyntaxKind.VariableDeclaration: {
const hasExclamationToken = !!node.exclamationToken;
if (hasExclamationToken) {
if (node.initializer) {
throw createError(
node,
'Declarations with initializers cannot also have definite assignment assertions.',
);
} else if (node.name.kind !== SyntaxKind.Identifier || !node.type) {
throw createError(
node,
'Declarations with definite assignment assertions must also have type annotations.',
);
}
}
if (node.parent.kind === SyntaxKind.VariableDeclarationList) {
const variableDeclarationList = node.parent;
const kind = getDeclarationKind(variableDeclarationList);
if (kind === 'using' || kind === 'await using') {
if (
variableDeclarationList.parent.kind === SyntaxKind.ForInStatement
) {
throw createError(
variableDeclarationList,
`The left-hand side of a 'for...in' statement cannot be a '${kind}' declaration.`,
);
}
if (
variableDeclarationList.parent.kind === SyntaxKind.ForStatement ||
variableDeclarationList.parent.kind === SyntaxKind.VariableStatement
) {
if (!node.initializer) {
throw createError(
node,
`'${kind}' declarations must be initialized.`,
);
}
if (node.name.kind !== SyntaxKind.Identifier) {
throw createError(
node.name,
`'${kind}' declarations may not have binding patterns.`,
);
}
}
}
if (
variableDeclarationList.parent.kind === SyntaxKind.VariableStatement
) {
const variableStatement = variableDeclarationList.parent;
const hasDeclareKeyword = hasModifier(
SyntaxKind.DeclareKeyword,
variableStatement,
);
// Definite assignment only allowed for non-declare let and var
if (
(hasDeclareKeyword ||
['await using', 'const', 'using'].includes(kind)) &&
hasExclamationToken
) {
throw createError(
node,
`A definite assignment assertion '!' is not permitted in this context.`,
);
}
if (
hasDeclareKeyword &&
node.initializer &&
(['let', 'var'].includes(kind) || node.type)
) {
throw createError(
node,
`Initializers are not permitted in ambient contexts.`,
);
}
// Theoretically, only certain initializers are allowed for declare const,
// (TS1254: A 'const' initializer in an ambient context must be a string
// or numeric literal or literal enum reference.) but we just allow
// all expressions
// Note! No-declare does not mean the variable is not ambient, because
// it can be further nested in other declare contexts. Therefore we cannot
// check for const initializers.
}
}
break;
}
case SyntaxKind.VariableStatement: {
const declarations = node.declarationList.declarations;
if (!declarations.length) {
throw createError(
node,
'A variable declaration list must have at least one variable declarator.',
);
}
break;
}
case SyntaxKind.PropertyAssignment: {
// eslint-disable-next-line @typescript-eslint/no-deprecated
const { exclamationToken, questionToken } = node;
if (questionToken) {
throw createError(
questionToken,
'A property assignment cannot have a question token.',
);
}
if (exclamationToken) {
throw createError(
exclamationToken,
'A property assignment cannot have an exclamation token.',
);
}
break;
}
case SyntaxKind.ShorthandPropertyAssignment: {
// eslint-disable-next-line @typescript-eslint/no-deprecated
const { exclamationToken, modifiers, questionToken } = node;
if (modifiers) {
throw createError(
modifiers[0],
'A shorthand property assignment cannot have modifiers.',
);
}
if (questionToken) {
throw createError(
questionToken,
'A shorthand property assignment cannot have a question token.',
);
}
if (exclamationToken) {
throw createError(
exclamationToken,
'A shorthand property assignment cannot have an exclamation token.',
);
}
break;
}
case SyntaxKind.PropertyDeclaration: {
const isAbstract = hasModifier(SyntaxKind.AbstractKeyword, node);
if (isAbstract && node.initializer) {
throw createError(
node.initializer,
`Abstract property cannot have an initializer.`,
);
}
const isDefinite = !!node.exclamationToken;
if (isDefinite && isAbstract) {
throw createError(
node.exclamationToken,
`A definite assignment assertion '!' is not permitted in this context.`,
);
}
if (isDefinite && !node.type) {
throw createError(
node,
`Declarations with definite assignment assertions must also have type annotations.`,
);
}
if (isDefinite && node.initializer) {
throw createError(
node,
`Declarations with initializers cannot also have definite assignment assertions.`,
);
}
if (
node.name.kind === SyntaxKind.StringLiteral &&
node.name.text === 'constructor'
) {
throw createError(
node.name,
"Classes may not have a field named 'constructor'.",
);
}
break;
}
case SyntaxKind.TaggedTemplateExpression:
if (node.tag.flags & ts.NodeFlags.OptionalChain) {
throw createError(
node,
'Tagged template expressions are not permitted in an optional chain.',
);
}
break;
case SyntaxKind.BinaryExpression:
if (
node.operatorToken.kind !== SyntaxKind.InKeyword &&
node.left.kind === SyntaxKind.PrivateIdentifier
) {
throw createError(
node.left,
"Private identifiers cannot appear on the right-hand-side of an 'in' expression.",
);
} else if (node.right.kind === SyntaxKind.PrivateIdentifier) {
throw createError(
node.right,
"Private identifiers are only allowed on the left-hand-side of an 'in' expression.",
);
}
break;
case SyntaxKind.MappedType:
if (node.members && node.members.length > 0) {
throw createError(
node.members[0],
'A mapped type may not declare properties or methods.',
);
}
break;
case SyntaxKind.PropertySignature: {
// eslint-disable-next-line @typescript-eslint/no-deprecated
const { initializer } = node;
if (initializer) {
throw createError(
initializer,
'A property signature cannot have an initializer.',
);
}
break;
}
case SyntaxKind.FunctionType: {
// eslint-disable-next-line @typescript-eslint/no-deprecated
const { modifiers } = node;
if (modifiers) {
throw createError(
modifiers[0],
'A function type cannot have modifiers.',
);
}
break;
}
case SyntaxKind.EnumMember: {
const computed = node.name.kind === ts.SyntaxKind.ComputedPropertyName;
if (computed) {
throw createError(
node.name,
'Computed property names are not allowed in enums.',
);
}
if (
node.name.kind === SyntaxKind.NumericLiteral ||
node.name.kind === SyntaxKind.BigIntLiteral
) {
throw createError(
node.name,
'An enum member cannot have a numeric name.',
);
}
break;
}
case SyntaxKind.ExternalModuleReference:
if (node.expression.kind !== SyntaxKind.StringLiteral) {
throw createError(node.expression, 'String literal expected.');
}
break;
case SyntaxKind.PrefixUnaryExpression:
case SyntaxKind.PostfixUnaryExpression: {
const operator = getTextForTokenKind(node.operator);
/**
* ESTree uses UpdateExpression for ++/--
*/
if (
(operator === '++' || operator === '--') &&
!isValidAssignmentTarget(node.operand)
) {
throw createError(
node.operand,
'Invalid left-hand side expression in unary operation',
);
}
break;
}
case SyntaxKind.ImportDeclaration: {
const { importClause } = node;
const importPhase = getImportClausePhaseModifier(importClause);
if (
importPhase === 'type' &&
importClause?.name &&
importClause.namedBindings
) {
throw createError(
importClause,
'A type-only import can specify a default import or named bindings, but not both.',
);
}
const isNamedImport =
importClause?.namedBindings?.kind === SyntaxKind.NamedImports;
const isDefaultImport = !!importClause?.name;
if (importPhase === 'defer' && isNamedImport) {
throw createError(
importClause,
'Named imports are not allowed in a deferred import.',
);
}
if (importPhase === 'defer' && isDefaultImport) {
throw createError(
importClause,
'Default imports are not allowed in a deferred import.',
);
}
assertModuleSpecifier(node, false);
break;
}
case SyntaxKind.ExportDeclaration:
assertModuleSpecifier(
node,
node.exportClause?.kind === SyntaxKind.NamedExports,
);
break;
case SyntaxKind.ExportSpecifier: {
const local = node.propertyName ?? node.name;
if (
local.kind === SyntaxKind.StringLiteral &&
parent.kind === SyntaxKind.ExportDeclaration &&
parent.moduleSpecifier?.kind !== SyntaxKind.StringLiteral
) {
throw createError(
local,
'A string literal cannot be used as a local exported binding without `from`.',
);
}
break;
}
case SyntaxKind.CallExpression:
if (
node.expression.kind === SyntaxKind.ImportKeyword &&
node.arguments.length !== 1 &&
node.arguments.length !== 2
) {
throw createError(
node.arguments.length > 1 ? node.arguments[2] : node,
'Dynamic import requires exactly one or two arguments.',
);
}
break;
case SyntaxKind.ClassDeclaration:
if (
!node.name &&
(!hasModifier(ts.SyntaxKind.ExportKeyword, node) ||
!hasModifier(ts.SyntaxKind.DefaultKeyword, node))
) {
throw createError(
node,
"A class declaration without the 'default' modifier must have a name.",
);
}
// intentional fallthrough
case SyntaxKind.ClassExpression: {
const heritageClauses = node.heritageClauses ?? [];
let seenExtendsClause = false;
let seenImplementsClause = false;
for (const heritageClause of heritageClauses) {
const { token, types } = heritageClause;
if (types.length === 0) {
throw createError(
heritageClause,
`'${ts.tokenToString(token)}' list cannot be empty.`,
);
}
if (token === SyntaxKind.ExtendsKeyword) {
if (seenExtendsClause) {
throw createError(heritageClause, "'extends' clause already seen.");
}
if (seenImplementsClause) {
throw createError(
heritageClause,
"'extends' clause must precede 'implements' clause.",
);
}
if (types.length > 1) {
throw createError(
types[1],
'Classes can only extend a single class.',
);
}
seenExtendsClause = true;
} else {
// `implements`
if (seenImplementsClause) {
throw createError(
heritageClause,
"'implements' clause already seen.",
);
}
for (const heritageType of heritageClause.types) {
if (
!isEntityNameExpression(heritageType.expression) ||
ts.isOptionalChain(heritageType.expression)
) {
throw createError(
heritageType,
'A class can only implement an identifier/qualified-name with optional type arguments.',
);
}
}
seenImplementsClause = true;
}
}
break;
}
case SyntaxKind.InterfaceDeclaration: {
const interfaceHeritageClauses = node.heritageClauses ?? [];
let seenExtendsClause = false;
for (const heritageClause of interfaceHeritageClauses) {
const { token, types } = heritageClause;
if (token === SyntaxKind.ImplementsKeyword) {
throw createError(
heritageClause,
"Interface declaration cannot have 'implements' clause.",
);
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (token !== SyntaxKind.ExtendsKeyword) {
throw createError(heritageClause, 'Unexpected token.');
}
if (types.length === 0) {
throw createError(
heritageClause,
`'${ts.tokenToString(token)}' list cannot be empty.`,
);
}
if (seenExtendsClause) {
throw createError(heritageClause, "'extends' clause already seen.");
}
seenExtendsClause = true;
for (const heritageType of heritageClause.types) {
if (
!isEntityNameExpression(heritageType.expression) ||
ts.isOptionalChain(heritageType.expression)
) {
throw createError(
heritageType,
'Interface declaration can only extend an identifier/qualified name with optional type arguments.',
);
}
}
}
break;
}
case SyntaxKind.GetAccessor:
case SyntaxKind.SetAccessor:
if (
node.parent.kind === SyntaxKind.InterfaceDeclaration ||
node.parent.kind === SyntaxKind.TypeLiteral
) {
return;
}
// otherwise, it is a non-type accessor - intentional fallthrough
case SyntaxKind.MethodDeclaration: {
const isAbstract = hasModifier(SyntaxKind.AbstractKeyword, node);
if (isAbstract && node.body) {
throw createError(
node.name,
node.kind === SyntaxKind.GetAccessor ||
node.kind === SyntaxKind.SetAccessor
? 'An abstract accessor cannot have an implementation.'
: `Method '${declarationNameToString(node.name)}' cannot have an implementation because it is marked abstract.`,
);
}
break;
}
case SyntaxKind.MetaProperty: {
const metaObject = getTextForTokenKind(node.keywordToken);
const metaPropertyName = node.name.text;
if (metaObject === 'import') {
if (
metaPropertyName === 'defer' &&
node.parent.kind !== SyntaxKind.CallExpression
) {
throw createError(
node,
"'import.defer' is only valid when called. Use 'import.defer()' instead.",
);
}
if (metaPropertyName !== 'meta') {
throw createError(
node,
`'${metaPropertyName}' is not a valid meta-property for keyword 'import'.`,
);
}
}
break;
}
case SyntaxKind.ObjectLiteralExpression: {
if (!allowPattern) {
for (const property of node.properties) {
if (
(property.kind === SyntaxKind.GetAccessor ||
property.kind === SyntaxKind.SetAccessor ||
property.kind === SyntaxKind.MethodDeclaration) &&
!property.body
) {
throw createError(
property.end - 1,
"'{' expected.",
node.getSourceFile(),
);
}
}
}
break;
}
case SyntaxKind.ImportEqualsDeclaration:
if (
node.isTypeOnly &&
node.moduleReference.kind !== SyntaxKind.ExternalModuleReference
) {
throw createError(node, "An import alias cannot use 'import type'");
}
break;
case SyntaxKind.ModuleDeclaration: {
if (node.flags & ts.NodeFlags.GlobalAugmentation) {
const { body } = node;
if (body == null || body.kind === SyntaxKind.ModuleDeclaration) {
throw createError(node.body ?? node, 'Expected a valid module body');
}
const { name } = node;
if (name.kind !== ts.SyntaxKind.Identifier) {
throw createError(
name,
'global module augmentation must have an Identifier id',
);
}
return;
}
if (ts.isStringLiteral(node.name)) {
return;
}
if (node.body == null) {
throw createError(node, 'Expected a module body');
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Fixme: confirm if it's possible
if (node.name.kind !== ts.SyntaxKind.Identifier) {
throw createError(node.name, '`namespace`s must have an Identifier id');
}
break;
}
case SyntaxKind.ForInStatement:
case SyntaxKind.ForOfStatement: {
checkForStatementDeclaration(node);
break;
}
// No default
}
}
checkForStatementDeclaration(node: ts.ForInStatement | ts.ForOfStatement): void¶
Parameters:
nodets.ForInStatement | ts.ForOfStatement
Returns: void
Calls:
ts.isVariableDeclarationListcreateError (from ./node-utils)isValidAssignmentTarget (from ./node-utils)
Code
function checkForStatementDeclaration(
node: ts.ForInStatement | ts.ForOfStatement,
): void {
const { initializer, kind } = node;
const loop = kind === SyntaxKind.ForInStatement ? 'for...in' : 'for...of';
if (ts.isVariableDeclarationList(initializer)) {
if (initializer.declarations.length !== 1) {
throw createError(
initializer,
`Only a single variable declaration is allowed in a '${loop}' statement.`,
);
}
const declaration = initializer.declarations[0];
if (declaration.initializer) {
throw createError(
declaration,
`The variable declaration of a '${loop}' statement cannot have an initializer.`,
);
} else if (declaration.type) {
throw createError(
declaration,
`The variable declaration of a '${loop}' statement cannot have a type annotation.`,
);
}
} else if (
!isValidAssignmentTarget(initializer) &&
initializer.kind !== SyntaxKind.ObjectLiteralExpression &&
initializer.kind !== SyntaxKind.ArrayLiteralExpression
) {
throw createError(
initializer,
`The left-hand side of a '${loop}' statement must be a variable or a property access.`,
);
}
}
assertModuleSpecifier(node: ts.ExportDeclaration | ts.ImportDeclaraβ¦, allowNull: boolean): void¶
Parameters:
nodets.ExportDeclaration | ts.ImportDeclarationallowNullboolean
Returns: void
Calls:
createError (from ./node-utils)
Code
function assertModuleSpecifier(
node: ts.ExportDeclaration | ts.ImportDeclaration,
allowNull: boolean,
) {
if (!allowNull && node.moduleSpecifier == null) {
throw createError(node, 'Module specifier must be a string literal.');
}
if (
node.moduleSpecifier &&
node.moduleSpecifier.kind !== SyntaxKind.StringLiteral
) {
throw createError(
node.moduleSpecifier,
'Module specifier must be a string literal.',
);
}
}
Generated by Syntax Scribe