Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(typescript-estree): allow erroring on TypeScript syntactic diagn… #6271

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 0 additions & 2 deletions packages/parser/tests/lib/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ describe('parser', () => {
filePath: 'isolated-file.src.ts',
project: 'tsconfig.json',
errorOnUnknownASTType: false,
errorOnTypeScriptSyntacticAndSemanticIssues: false,
tsconfigRootDir: 'tests/fixtures/services',
extraFileExtensions: ['.foo'],
};
Expand Down Expand Up @@ -90,7 +89,6 @@ describe('parser', () => {
filePath: 'isolated-file.src.ts',
project: 'tsconfig.json',
errorOnUnknownASTType: false,
errorOnTypeScriptSyntacticAndSemanticIssues: false,
tsconfigRootDir: 'tests/fixtures/services',
extraFileExtensions: ['.foo'],
};
Expand Down
16 changes: 14 additions & 2 deletions packages/types/src/parser-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ type EcmaVersion =

type SourceType = 'script' | 'module';

enum TypeScriptIssueDetection {
None = 0,
Syntactic = 1,
SyntacticOrSemantic = 2,
}

interface ParserOptions {
ecmaFeatures?: {
globalReturn?: boolean;
Expand All @@ -44,7 +50,7 @@ interface ParserOptions {
// typescript-estree specific
comment?: boolean;
debugLevel?: DebugLevel;
errorOnTypeScriptSyntacticAndSemanticIssues?: boolean;
errorOnTypeScriptIssues?: TypeScriptIssueDetection;
errorOnUnknownASTType?: boolean;
EXPERIMENTAL_useSourceOfProjectReferenceRedirect?: boolean; // purposely undocumented for now
extraFileExtensions?: string[];
Expand All @@ -62,4 +68,10 @@ interface ParserOptions {
[additionalProperties: string]: unknown;
}

export { DebugLevel, EcmaVersion, ParserOptions, SourceType };
export {
DebugLevel,
EcmaVersion,
ParserOptions,
SourceType,
TypeScriptIssueDetection,
};
1 change: 1 addition & 0 deletions packages/typescript-estree/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { createProgramFromConfigFile as createProgram } from './create-program/u
export * from './create-program/getScriptKind';
export { typescriptVersionIsAtLeast } from './version-check';
export * from './getModifiers';
export { TypeScriptIssueDetection } from '@typescript-eslint/types';

// re-export for backwards-compat
export { visitorKeys } from '@typescript-eslint/visitor-keys';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TypeScriptIssueDetection } from '@typescript-eslint/types';
import debug from 'debug';
import { sync as globSync } from 'globby';
import isGlob from 'is-glob';
Expand Down Expand Up @@ -41,7 +42,8 @@ export function createParseSettings(
: Array.isArray(options.debugLevel)
? new Set(options.debugLevel)
: new Set(),
errorOnTypeScriptSyntacticAndSemanticIssues: false,
errorOnTypeScriptIssues:
options.errorOnTypeScriptIssues ?? TypeScriptIssueDetection.None,
errorOnUnknownASTType: options.errorOnUnknownASTType === true,
EXPERIMENTAL_useSourceOfProjectReferenceRedirect:
options.EXPERIMENTAL_useSourceOfProjectReferenceRedirect === true,
Expand Down
3 changes: 2 additions & 1 deletion packages/typescript-estree/src/parseSettings/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { TypeScriptIssueDetection } from '@typescript-eslint/types';
import type * as ts from 'typescript';

import type { CanonicalPath } from '../create-program/shared';
Expand Down Expand Up @@ -44,7 +45,7 @@ export interface MutableParseSettings {
/**
* Whether to error if TypeScript reports a semantic or syntactic error diagnostic.
*/
errorOnTypeScriptSyntacticAndSemanticIssues: boolean;
errorOnTypeScriptIssues: TypeScriptIssueDetection;

/**
* Whether to error if an unknown AST node type is encountered.
Expand Down
7 changes: 5 additions & 2 deletions packages/typescript-estree/src/parser-options.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { DebugLevel } from '@typescript-eslint/types';
import type {
DebugLevel,
TypeScriptIssueDetection,
} from '@typescript-eslint/types';
import type * as ts from 'typescript';

import type { TSESTree, TSESTreeToTSNode, TSNode, TSToken } from './ts-estree';
Expand Down Expand Up @@ -78,7 +81,7 @@ interface ParseAndGenerateServicesOptions extends ParseOptions {
/**
* Causes the parser to error if the TypeScript compiler returns any unexpected syntax/semantic errors.
*/
errorOnTypeScriptSyntacticAndSemanticIssues?: boolean;
errorOnTypeScriptIssues?: TypeScriptIssueDetection;

/**
* ***EXPERIMENTAL FLAG*** - Use this at your own risk.
Expand Down
22 changes: 8 additions & 14 deletions packages/typescript-estree/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ function parseWithNodeMapsInternal<T extends TSESTreeOptions = TSESTreeOptions>(
/**
* Ensure users do not attempt to use parse() when they need parseAndGenerateServices()
*/
if (options?.errorOnTypeScriptSyntacticAndSemanticIssues) {
if (options?.errorOnTypeScriptIssues) {
throw new Error(
`"errorOnTypeScriptSyntacticAndSemanticIssues" is only supported for parseAndGenerateServices()`,
`"errorOnTypeScriptIssues" is only supported for parseAndGenerateServices()`,
);
}

Expand Down Expand Up @@ -138,16 +138,6 @@ function parseAndGenerateServices<T extends TSESTreeOptions = TSESTreeOptions>(
*/
const parseSettings = createParseSettings(code, options);

if (typeof options !== 'undefined') {
if (
typeof options.errorOnTypeScriptSyntacticAndSemanticIssues ===
'boolean' &&
options.errorOnTypeScriptSyntacticAndSemanticIssues
) {
parseSettings.errorOnTypeScriptSyntacticAndSemanticIssues = true;
}
}

/**
* If this is a single run in which the user has not provided any existing programs but there
* are programs which need to be created from the provided "project" option,
Expand Down Expand Up @@ -223,8 +213,12 @@ function parseAndGenerateServices<T extends TSESTreeOptions = TSESTreeOptions>(
* Even if TypeScript parsed the source code ok, and we had no problems converting the AST,
* there may be other syntactic or semantic issues in the code that we can optionally report on.
*/
if (program && parseSettings.errorOnTypeScriptSyntacticAndSemanticIssues) {
const error = getFirstSemanticOrSyntacticError(program, ast);
if (parseSettings.errorOnTypeScriptIssues) {
const error = getFirstSemanticOrSyntacticError(
program,
ast,
parseSettings.errorOnTypeScriptIssues,
);
if (error) {
throw convertError(error);
}
Expand Down
83 changes: 15 additions & 68 deletions packages/typescript-estree/src/semantic-or-syntactic-errors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TypeScriptIssueDetection } from '@typescript-eslint/types';
import type {
Diagnostic,
DiagnosticWithLocation,
Expand All @@ -10,33 +11,30 @@ export interface SemanticOrSyntacticError extends Diagnostic {
message: string;
}

/**
* By default, diagnostics from the TypeScript compiler contain all errors - regardless of whether
* they are related to generic ECMAScript standards, or TypeScript-specific constructs.
*
* Therefore, we filter out all diagnostics, except for the ones we explicitly want to consider when
* the user opts in to throwing errors on semantic issues.
*/
export function getFirstSemanticOrSyntacticError(
program: Program,
ast: SourceFile,
issueDetection: TypeScriptIssueDetection,
): SemanticOrSyntacticError | undefined {
if (!issueDetection) {
return undefined;
}

try {
const supportedSyntacticDiagnostics = whitelistSupportedDiagnostics(
program.getSyntacticDiagnostics(ast),
);
const supportedSyntacticDiagnostics = program.getSyntacticDiagnostics(ast);
if (supportedSyntacticDiagnostics.length) {
return convertDiagnosticToSemanticOrSyntacticError(
supportedSyntacticDiagnostics[0],
);
}
const supportedSemanticDiagnostics = whitelistSupportedDiagnostics(
program.getSemanticDiagnostics(ast),
);
if (supportedSemanticDiagnostics.length) {
return convertDiagnosticToSemanticOrSyntacticError(
supportedSemanticDiagnostics[0],
);

if (issueDetection === TypeScriptIssueDetection.SyntacticOrSemantic) {
const supportedSemanticDiagnostics = program.getSemanticDiagnostics(ast);
if (supportedSemanticDiagnostics.length) {
return convertDiagnosticToSemanticOrSyntacticError(
supportedSemanticDiagnostics[0],
);
}
}
return undefined;
} catch (e) {
Expand All @@ -57,57 +55,6 @@ export function getFirstSemanticOrSyntacticError(
}
}

function whitelistSupportedDiagnostics(
diagnostics: readonly (DiagnosticWithLocation | Diagnostic)[],
): readonly (DiagnosticWithLocation | Diagnostic)[] {
return diagnostics.filter(diagnostic => {
switch (diagnostic.code) {
case 1013: // "A rest parameter or binding pattern may not have a trailing comma."
case 1014: // "A rest parameter must be last in a parameter list."
case 1044: // "'{0}' modifier cannot appear on a module or namespace element."
case 1045: // "A '{0}' modifier cannot be used with an interface declaration."
case 1048: // "A rest parameter cannot have an initializer."
case 1049: // "A 'set' accessor must have exactly one parameter."
case 1070: // "'{0}' modifier cannot appear on a type member."
case 1071: // "'{0}' modifier cannot appear on an index signature."
case 1085: // "Octal literals are not available when targeting ECMAScript 5 and higher. Use the syntax '{0}'."
case 1090: // "'{0}' modifier cannot appear on a parameter."
case 1096: // "An index signature must have exactly one parameter."
case 1097: // "'{0}' list cannot be empty."
case 1098: // "Type parameter list cannot be empty."
case 1099: // "Type argument list cannot be empty."
case 1117: // "An object literal cannot have multiple properties with the same name in strict mode."
case 1121: // "Octal literals are not allowed in strict mode."
case 1123: // "Variable declaration list cannot be empty."
case 1141: // "String literal expected."
case 1162: // "An object member cannot be declared optional."
case 1164: // "Computed property names are not allowed in enums."
case 1172: // "'extends' clause already seen."
case 1173: // "'extends' clause must precede 'implements' clause."
case 1175: // "'implements' clause already seen."
case 1176: // "Interface declaration cannot have 'implements' clause."
case 1190: // "The variable declaration of a 'for...of' statement cannot have an initializer."
case 1196: // "Catch clause variable type annotation must be 'any' or 'unknown' if specified."
case 1200: // "Line terminator not permitted before arrow."
case 1206: // "Decorators are not valid here."
case 1211: // "A class declaration without the 'default' modifier must have a name."
case 1242: // "'abstract' modifier can only appear on a class, method, or property declaration."
case 1246: // "An interface property cannot have an initializer."
case 1255: // "A definite assignment assertion '!' is not permitted in this context."
case 1308: // "'await' expression is only allowed within an async function."
case 2364: // "The left-hand side of an assignment expression must be a variable or a property access."
case 2369: // "A parameter property is only allowed in a constructor implementation."
case 2452: // "An enum member cannot have a numeric name."
case 2462: // "A rest element must be last in a destructuring pattern."
case 8017: // "Octal literal types must use ES2015 syntax. Use the syntax '{0}'."
case 17012: // "'{0}' is not a valid meta-property for keyword '{1}'. Did you mean '{2}'?"
case 17013: // "Meta-property '{0}' is only allowed in the body of a function declaration, function expression, or constructor."
return true;
}
return false;
});
}

function convertDiagnosticToSemanticOrSyntacticError(
diagnostic: Diagnostic,
): SemanticOrSyntacticError {
Expand Down
3 changes: 2 additions & 1 deletion packages/typescript-estree/tests/ast-alignment/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type babelParser from '@babel/parser';
import type { ParserPlugin } from '@babel/parser';
import type { File } from '@babel/types';
import type { TSESTree } from '@typescript-eslint/types';
import { TypeScriptIssueDetection } from '@typescript-eslint/types';

import type { TSError } from '../../src/node-utils';
import type { AST } from '../../src/parser';
Expand Down Expand Up @@ -64,7 +65,7 @@ function parseWithTypeScriptESTree(text: string, jsx = true): AST<any> {
* two parsers. By default, the TypeScript compiler is much more
* forgiving.
*/
errorOnTypeScriptSyntacticAndSemanticIssues: true,
errorOnTypeScriptIssues: TypeScriptIssueDetection.Syntactic,
jsx,
});
return result.ast;
Expand Down