Skip to content

Commit

Permalink
feat(typescript-estree): add support for class static blocks (#3730)
Browse files Browse the repository at this point in the history
  • Loading branch information
sosukesuzuki committed Aug 28, 2021
1 parent 043462e commit f81831b
Show file tree
Hide file tree
Showing 29 changed files with 805 additions and 38 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -233,7 +233,7 @@ The latest version under the `canary` tag **(latest commit to master)** is:

## Supported TypeScript Version

**The version range of TypeScript currently supported by this parser is `>=3.3.1 <4.4.0`.**
**The version range of TypeScript currently supported by this parser is `>=3.3.1 <4.5.0`.**

These versions are what we test against.

Expand Down
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -121,12 +121,12 @@
"ts-jest": "^27.0.1",
"ts-node": "^10.0.0",
"tslint": "^6.1.3",
"typescript": ">=3.3.1 <4.4.0"
"typescript": ">=3.3.1 <4.5.0"
},
"resolutions": {
"@types/node": "^15.6.1",
"jest-diff": "^27.0.0",
"pretty-format": "^27.0.0",
"typescript": "4.3.5"
"typescript": "4.4.2"
}
}
1 change: 1 addition & 0 deletions packages/ast-spec/src/ast-node-types.ts
Expand Up @@ -68,6 +68,7 @@ export enum AST_NODE_TYPES {
ReturnStatement = 'ReturnStatement',
SequenceExpression = 'SequenceExpression',
SpreadElement = 'SpreadElement',
StaticBlock = 'StaticBlock',
Super = 'Super',
SwitchCase = 'SwitchCase',
SwitchStatement = 'SwitchStatement',
Expand Down
8 changes: 8 additions & 0 deletions packages/ast-spec/src/element/StaticBlock/spec.ts
@@ -0,0 +1,8 @@
import type { AST_NODE_TYPES } from '../../ast-node-types';
import type { BaseNode } from '../../base/BaseNode';
import type { Statement } from '../../unions/Statement';

export interface StaticBlock extends BaseNode {
type: AST_NODE_TYPES.StaticBlock;
body: Statement[];
}
1 change: 1 addition & 0 deletions packages/ast-spec/src/element/spec.ts
Expand Up @@ -2,6 +2,7 @@ export * from './ClassProperty/spec';
export * from './MethodDefinition/spec';
export * from './Property/spec';
export * from './SpreadElement/spec';
export * from './StaticBlock/spec';
export * from './TSAbstractClassProperty/spec';
export * from './TSAbstractMethodDefinition/spec';
export * from './TSCallSignatureDeclaration/spec';
Expand Down
Expand Up @@ -45,7 +45,7 @@ export interface PunctuatorTokenToText {
[SyntaxKind.AtToken]: '@';
[SyntaxKind.QuestionQuestionToken]: '??';
[SyntaxKind.BacktickToken]: '`';
// [SyntaxKind.HashToken]: '#'; // new in PunctuationSyntaxKind in TS 4.4
[SyntaxKind.HashToken]: '#';
[SyntaxKind.EqualsToken]: '=';
[SyntaxKind.PlusEqualsToken]: '+=';
[SyntaxKind.MinusEqualsToken]: '-=';
Expand All @@ -58,8 +58,8 @@ export interface PunctuatorTokenToText {
[SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken]: '>>>=';
[SyntaxKind.AmpersandEqualsToken]: '&=';
[SyntaxKind.BarEqualsToken]: '|=';
[SyntaxKind.BarBarEqualsToken]: '||='; // included in PunctuationSyntaxKind in TS 4.4
[SyntaxKind.AmpersandAmpersandEqualsToken]: '&&='; // included in PunctuationSyntaxKind in TS 4.4
[SyntaxKind.QuestionQuestionEqualsToken]: '??='; // included in PunctuationSyntaxKind in TS 4.4
[SyntaxKind.BarBarEqualsToken]: '||=';
[SyntaxKind.AmpersandAmpersandEqualsToken]: '&&=';
[SyntaxKind.QuestionQuestionEqualsToken]: '??=';
[SyntaxKind.CaretEqualsToken]: '^=';
}
2 changes: 2 additions & 0 deletions packages/ast-spec/src/unions/ClassElement.ts
@@ -1,12 +1,14 @@
import type { ClassProperty } from '../element/ClassProperty/spec';
import type { MethodDefinition } from '../element/MethodDefinition/spec';
import type { StaticBlock } from '../element/StaticBlock/spec';
import type { TSAbstractClassProperty } from '../element/TSAbstractClassProperty/spec';
import type { TSAbstractMethodDefinition } from '../element/TSAbstractMethodDefinition/spec';
import type { TSIndexSignature } from '../element/TSIndexSignature/spec';

export type ClassElement =
| ClassProperty
| MethodDefinition
| StaticBlock
| TSAbstractClassProperty
| TSAbstractMethodDefinition
| TSIndexSignature;
2 changes: 2 additions & 0 deletions packages/ast-spec/src/unions/Node.ts
Expand Up @@ -16,6 +16,7 @@ import type { ClassProperty } from '../element/ClassProperty/spec';
import type { MethodDefinition } from '../element/MethodDefinition/spec';
import type { Property } from '../element/Property/spec';
import type { SpreadElement } from '../element/SpreadElement/spec';
import type { StaticBlock } from '../element/StaticBlock/spec';
import type { TSAbstractClassProperty } from '../element/TSAbstractClassProperty/spec';
import type { TSAbstractMethodDefinition } from '../element/TSAbstractMethodDefinition/spec';
import type { TSCallSignatureDeclaration } from '../element/TSCallSignatureDeclaration/spec';
Expand Down Expand Up @@ -235,6 +236,7 @@ export type Node =
| ReturnStatement
| SequenceExpression
| SpreadElement
| StaticBlock
| Super
| SwitchCase
| SwitchStatement
Expand Down
Expand Up @@ -179,7 +179,7 @@ export class OffsetStorage {
fromToken.range[1] <= range[1];
// this has to be before the delete + insert below or else you'll get into a cycle
const fromTokenDescriptor = fromTokenIsInRange
? this.getOffsetDescriptor(fromToken!)
? this.getOffsetDescriptor(fromToken)
: null;

// First, remove any existing nodes in the range from the tree.
Expand All @@ -193,8 +193,8 @@ export class OffsetStorage {
* even if it's in the current range.
*/
if (fromTokenIsInRange) {
this.tree.insert(fromToken!.range[0], fromTokenDescriptor!);
this.tree.insert(fromToken!.range[1], descriptorToInsert);
this.tree.insert(fromToken.range[0], fromTokenDescriptor!);
this.tree.insert(fromToken.range[1], descriptorToInsert);
}

/*
Expand Down
2 changes: 1 addition & 1 deletion packages/parser/tests/tools/test-utils.ts
Expand Up @@ -59,7 +59,7 @@ export function createSnapshotTestBlock(
* AST_NODE_TYPE, we rethrow to cause the test to fail
*/
if (/Unknown AST_NODE_TYPE/.exec((error as Error).message)) {
throw new Error(error);
throw error;
}
expect(parse).toThrowErrorMatchingSnapshot();
}
Expand Down
5 changes: 4 additions & 1 deletion packages/scope-manager/tests/fixtures.test.ts
Expand Up @@ -139,7 +139,10 @@ function nestDescribe(

try {
makeDir.sync(fixture.snapshotPath);
} catch (e) {
} catch (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
e: any
) {
if ('code' in e && e.code === 'EEXIST') {
// already exists - ignored
} else {
Expand Down
@@ -0,0 +1,8 @@
class Foo {
static count = 0;
static {
if (someCondition()) {
count++;
}
}
}
13 changes: 12 additions & 1 deletion packages/typescript-estree/src/convert.ts
Expand Up @@ -311,7 +311,11 @@ export class Converter {
*/
private convertBodyExpressions(
nodes: ts.NodeArray<ts.Statement>,
parent: ts.SourceFile | ts.Block | ts.ModuleBlock,
parent:
| ts.SourceFile
| ts.Block
| ts.ModuleBlock
| ts.ClassStaticBlockDeclaration,
): TSESTree.Statement[] {
let allowDirectives = canContainDirective(parent);

Expand Down Expand Up @@ -2824,6 +2828,13 @@ export class Converter {
return result;
}

case SyntaxKind.ClassStaticBlockDeclaration: {
return this.createNode<TSESTree.StaticBlock>(node, {
type: AST_NODE_TYPES.StaticBlock,
body: this.convertBodyExpressions(node.body.statements, node),
});
}

default:
return this.deeplyCopy(node);
}
Expand Down
6 changes: 5 additions & 1 deletion packages/typescript-estree/src/node-utils.ts
Expand Up @@ -194,7 +194,11 @@ export function getLocFor(
* @returns returns true if node can contain directive
*/
export function canContainDirective(
node: ts.SourceFile | ts.Block | ts.ModuleBlock,
node:
| ts.SourceFile
| ts.Block
| ts.ModuleBlock
| ts.ClassStaticBlockDeclaration,
): boolean {
if (node.kind === ts.SyntaxKind.Block) {
switch (node.parent.kind) {
Expand Down
4 changes: 2 additions & 2 deletions packages/typescript-estree/src/parser.ts
Expand Up @@ -30,12 +30,12 @@ const log = debug('typescript-eslint:typescript-estree:parser');
* This needs to be kept in sync with the top-level README.md in the
* typescript-eslint monorepo
*/
const SUPPORTED_TYPESCRIPT_VERSIONS = '>=3.3.1 <4.4.0';
const SUPPORTED_TYPESCRIPT_VERSIONS = '>=3.3.1 <4.5.0';
/*
* The semver package will ignore prerelease ranges, and we don't want to explicitly document every one
* List them all separately here, so we can automatically create the full string
*/
const SUPPORTED_PRERELEASE_RANGES: string[] = ['4.3.0-beta', '4.3.1-rc'];
const SUPPORTED_PRERELEASE_RANGES: string[] = ['4.4.0-beta', '4.4.1-rc'];
const ACTIVE_TYPESCRIPT_VERSION = ts.version;
const isRunningSupportedTypeScriptVersion = semver.satisfies(
ACTIVE_TYPESCRIPT_VERSION,
Expand Down
Expand Up @@ -135,6 +135,7 @@ export interface EstreeToTsNodeTypes {
[AST_NODE_TYPES.ReturnStatement]: ts.ReturnStatement;
[AST_NODE_TYPES.SequenceExpression]: ts.BinaryExpression;
[AST_NODE_TYPES.SpreadElement]: ts.SpreadElement | ts.SpreadAssignment;
[AST_NODE_TYPES.StaticBlock]: ts.ClassStaticBlockDeclaration;
[AST_NODE_TYPES.Super]: ts.SuperExpression;
[AST_NODE_TYPES.SwitchCase]: ts.CaseClause | ts.DefaultClause;
[AST_NODE_TYPES.SwitchStatement]: ts.SwitchStatement;
Expand Down
1 change: 1 addition & 0 deletions packages/typescript-estree/src/ts-estree/ts-nodes.ts
Expand Up @@ -43,6 +43,7 @@ export type TSNode =
| ts.KeywordTypeNode // TODO: This node is bad, maybe we should report this
| ts.ImportTypeNode
| ts.ThisTypeNode
| ts.ClassStaticBlockDeclaration
// | ts.FunctionOrConstructorTypeNodeBase -> FunctionTypeNode, ConstructorTypeNode
| ts.ConstructorTypeNode
| ts.FunctionTypeNode
Expand Down
5 changes: 3 additions & 2 deletions packages/typescript-estree/tests/ast-alignment/parse.ts
Expand Up @@ -26,6 +26,7 @@ function parseWithBabelParser(text: string, jsx = true): File {
const plugins: ParserPlugin[] = [
'classProperties',
'decorators-legacy',
'classStaticBlock',
'estree',
'typescript',
];
Expand Down Expand Up @@ -61,7 +62,7 @@ function parseWithTypeScriptESTree(text: string, jsx = true): parser.AST<any> {
jsx,
});
return result.ast;
} catch (e) {
} catch (e: any) {
throw createError(e.message, e.lineNumber, e.column);
}
}
Expand Down Expand Up @@ -97,7 +98,7 @@ export function parse(
'Please provide a valid parser: either "typescript-estree" or "@babel/parser"',
);
}
} catch (error) {
} catch (error: any) {
const loc = error.loc as TSESTree.LineAndColumnData | undefined;
if (loc) {
error.codeFrame = codeFrameColumns(
Expand Down
10 changes: 10 additions & 0 deletions packages/typescript-estree/tests/ast-alignment/utils.ts
Expand Up @@ -250,6 +250,16 @@ export function preprocessBabylonAST(ast: BabelTypes.File): any {
}
}
},
/**
* Babel adds a `static` property to the StaticBlock when the
* `typescript` plugin and the `classStaticBlock` plugin are enabled.
* @see https://github.com/babel/babel/issues/13674
*/
StaticBlock(node: any) {
if (node.static != null) {
delete node.static;
}
},
},
);
}
Expand Down
5 changes: 4 additions & 1 deletion packages/typescript-estree/tests/ast-fixtures.test.ts
Expand Up @@ -58,7 +58,10 @@ function nestDescribe(

try {
makeDir.sync(fixture.snapshotPath);
} catch (e) {
} catch (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
e: any
) {
if ('code' in e && e.code === 'EEXIST') {
// already exists - ignored
} else {
Expand Down
Expand Up @@ -1408,8 +1408,8 @@ TSError {

exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" enabled fixtures/jsx/invalid-mismatched-closing-tag.src 1`] = `
TSError {
"column": 3,
"index": 3,
"column": 5,
"index": 5,
"lineNumber": 1,
"message": "Expected corresponding JSX closing tag for 'a'.",
}
Expand All @@ -1426,17 +1426,17 @@ TSError {

exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" enabled fixtures/jsx/invalid-mismatched-dot-tag-name.src 1`] = `
TSError {
"column": 7,
"index": 7,
"column": 9,
"index": 9,
"lineNumber": 1,
"message": "Expected corresponding JSX closing tag for 'a.b.c'.",
}
`;

exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" enabled fixtures/jsx/invalid-mismatched-namespace-tag.src 1`] = `
TSError {
"column": 5,
"index": 5,
"column": 7,
"index": 7,
"lineNumber": 1,
"message": "Expected corresponding JSX closing tag for 'a:b'.",
}
Expand Down Expand Up @@ -1742,6 +1742,8 @@ exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" e

exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" enabled fixtures/typescript/basics/class-multi-line-keyword-declare.src 1`] = `"TEST OUTPUT: No semantic or syntactic issues found"`;

exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" enabled fixtures/typescript/basics/class-static-blocks.src 1`] = `"TEST OUTPUT: No semantic or syntactic issues found"`;

exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" enabled fixtures/typescript/basics/class-with-accessibility-modifiers.src 1`] = `"TEST OUTPUT: No semantic or syntactic issues found"`;

exports[`Parse all fixtures with "errorOnTypeScriptSyntacticAndSemanticIssues" enabled fixtures/typescript/basics/class-with-constructor-and-modifier.src 1`] = `"TEST OUTPUT: No semantic or syntactic issues found"`;
Expand Down
10 changes: 8 additions & 2 deletions packages/typescript-estree/tests/lib/parse.test.ts
Expand Up @@ -50,7 +50,10 @@ describe('parseWithNodeMaps()', () => {
it('should have correct column number when strict mode error occurs', () => {
try {
parser.parseWithNodeMaps('function fn(a, a) {\n}');
} catch (err) {
} catch (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
err: any
) {
expect(err.column).toEqual(16);
}
});
Expand Down Expand Up @@ -505,7 +508,10 @@ describe('parseAndGenerateServices', () => {
/**
* Aligns paths between environments, node for windows uses `\`, for linux and mac uses `/`
*/
error.message = (error as Error).message.replace(/\\(?!["])/gm, '/');
(error as Error).message = (error as Error).message.replace(
/\\(?!["])/gm,
'/',
);
throw error;
}
};
Expand Down
Expand Up @@ -2,8 +2,8 @@

exports[`jsx invalid-mismatched-closing-tag.src 1`] = `
TSError {
"column": 3,
"index": 3,
"column": 5,
"index": 5,
"lineNumber": 1,
"message": "Expected corresponding JSX closing tag for 'a'.",
}
Expand Down
Expand Up @@ -2,8 +2,8 @@

exports[`jsx invalid-mismatched-dot-tag-name.src 1`] = `
TSError {
"column": 7,
"index": 7,
"column": 9,
"index": 9,
"lineNumber": 1,
"message": "Expected corresponding JSX closing tag for 'a.b.c'.",
}
Expand Down
Expand Up @@ -2,8 +2,8 @@

exports[`jsx invalid-mismatched-namespace-tag.src 1`] = `
TSError {
"column": 5,
"index": 5,
"column": 7,
"index": 7,
"lineNumber": 1,
"message": "Expected corresponding JSX closing tag for 'a:b'.",
}
Expand Down

0 comments on commit f81831b

Please sign in to comment.