Skip to content

feat(parser): support ecmaFeatures.jsx flag and tests #85

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

Merged
merged 3 commits into from
Jan 20, 2019
Merged
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
6 changes: 4 additions & 2 deletions packages/parser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ By far the most common case will be installing the [@typescript-eslint/eslint-pl

The following additional configuration options are available by specifying them in [`parserOptions`](https://eslint.org/docs/user-guide/configuring#specifying-parser-options) in your ESLint configuration file.

- **`jsx`** - default `false`. Enable parsing JSX when `true`. More details can be found [here](https://www.typescriptlang.org/docs/handbook/jsx.html).
- **`ecmaFeatures.jsx`** - default `false`. Enable parsing JSX when `true`. More details can be found [here](https://www.typescriptlang.org/docs/handbook/jsx.html).

- It's `false` on `*.ts` files regardless of this option.
- It's `true` on `*.tsx` files regardless of this option.
Expand All @@ -46,7 +46,9 @@ The following additional configuration options are available by specifying them
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"jsx": true,
"ecmaFeatures": {
"jsx": true
},
"useJSXTextNode": true
}
}
Expand Down
23 changes: 13 additions & 10 deletions packages/parser/src/parser-options.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
export interface ParserOptions {
useJSXTextNode?: boolean;
loc?: true;
range?: true;
tokens?: true;
filePath?: string;
loc?: boolean;
comment?: boolean;
range?: boolean;
tokens?: boolean;
sourceType?: 'script' | 'module';
ecmaVersion?: number;
ecmaFeatures?: {
globalReturn?: boolean;
jsx?: boolean;
};
/**
* @deprecated We should finalize the work from
* https://github.com/eslint/typescript-eslint-parser#595
*/
jsx?: boolean;
// ts-estree specific
filePath?: string;
project?: string | string[];
useJSXTextNode?: boolean;
errorOnUnknownASTType?: boolean;
errorOnTypeScriptSyntacticAndSemanticIssues?: boolean;
tsconfigRootDir?: string;
extraFileExtensions?: string[];
}
69 changes: 44 additions & 25 deletions packages/parser/src/parser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import traverser from 'eslint/lib/util/traverser';
import * as typescriptESTree from '@typescript-eslint/typescript-estree';
import {
AST_NODE_TYPES,
parseAndGenerateServices,
ParserOptions as ParserOptionsTsESTree
} from '@typescript-eslint/typescript-estree';
import { analyzeScope } from './analyze-scope';
import { ParserOptions } from './parser-options';
import { visitorKeys } from './visitor-keys';
Expand All @@ -20,52 +24,67 @@ interface ParseForESLintResult {
scopeManager: ReturnType<typeof analyzeScope>;
}

function validateBoolean(
value: boolean | undefined,
fallback: boolean = false
): boolean {
if (typeof value !== 'boolean') {
return fallback;
}
return value;
}

//------------------------------------------------------------------------------
// Public
//------------------------------------------------------------------------------

export const version = packageJSON.version;

export const Syntax = Object.freeze(AST_NODE_TYPES);

export function parse(code: string, options?: ParserOptions) {
return parseForESLint(code, options).ast;
}

export const Syntax = Object.freeze(typescriptESTree.AST_NODE_TYPES);

export function parseForESLint<T extends ParserOptions = ParserOptions>(
export function parseForESLint(
code: string,
options?: T | null
options?: ParserOptions | null
): ParseForESLintResult {
if (typeof options !== 'object' || options === null) {
options = { useJSXTextNode: true } as T;
} else if (typeof options.useJSXTextNode !== 'boolean') {
options = Object.assign({}, options, { useJSXTextNode: true });
}
if (typeof options.filePath === 'string') {
const tsx = options.filePath.endsWith('.tsx');
if (tsx || options.filePath.endsWith('.ts')) {
options = Object.assign({}, options, { jsx: tsx });
}
if (!options || typeof options !== 'object') {
options = {};
}

// https://eslint.org/docs/user-guide/configuring#specifying-parser-options
// if sourceType is not provided by default eslint expect that it will be set to "script"
options.sourceType = options.sourceType || 'script';
if (options.sourceType !== 'module' && options.sourceType !== 'script') {
options.sourceType = 'script';
}
if (typeof options.ecmaFeatures !== 'object') {
options.ecmaFeatures = {};
}

const parserOptions: ParserOptionsTsESTree = {};
Object.assign(parserOptions, options, {
useJSXTextNode: validateBoolean(options.useJSXTextNode, true),
jsx: validateBoolean(options.ecmaFeatures.jsx)
});

if (typeof options.filePath === 'string') {
const tsx = options.filePath.endsWith('.tsx');
if (tsx || options.filePath.endsWith('.ts')) {
parserOptions.jsx = tsx;
}
}

const { ast, services } = typescriptESTree.parseAndGenerateServices(
code,
options
);
const { ast, services } = parseAndGenerateServices(code, parserOptions);
ast.sourceType = options.sourceType;

traverser.traverse(ast, {
enter: (node: any) => {
enter(node: any) {
switch (node.type) {
// Function#body cannot be null in ESTree spec.
case 'FunctionExpression':
if (!node.body) {
node.type = `TSEmptyBody${
node.type
}` as typescriptESTree.AST_NODE_TYPES;
node.type = `TSEmptyBody${node.type}` as AST_NODE_TYPES;
}
break;
// no default
Expand Down
6 changes: 4 additions & 2 deletions packages/parser/tests/lib/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ describe('Comments', () => {
testFiles.forEach(filename => {
const code = fs.readFileSync(filename, 'utf8');
const config: ParserOptions = {
jsx: true,
sourceType: 'module'
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
};
it(
testUtils.formatSnapshotName(filename, FIXTURES_DIR),
Expand Down
4 changes: 3 additions & 1 deletion packages/parser/tests/lib/jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ describe('JSX', () => {
const code = fs.readFileSync(filename, 'utf8');
const config = {
useJSXTextNode,
jsx: true
ecmaFeatures: {
jsx: true
}
};
it(
testUtils.formatSnapshotName(filename, fixturesDir),
Expand Down
54 changes: 54 additions & 0 deletions packages/parser/tests/lib/parser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as typescriptESTree from '@typescript-eslint/typescript-estree';
import { parse, parseForESLint, Syntax } from '../../src/parser';
import * as scope from '../../src/analyze-scope';

describe('parser', () => {
it('parse() should return just the AST from parseForESLint()', () => {
Expand All @@ -15,11 +16,64 @@ describe('parser', () => {
it('parseForESLint() should set the sourceType to script, if an invalid one is provided', () => {
const code = 'const valid = true;';
const spy = jest.spyOn(typescriptESTree, 'parseAndGenerateServices');
const spyScope = jest.spyOn(scope, 'analyzeScope');
parseForESLint(code, { sourceType: 'foo' as any });
expect(spy).toHaveBeenCalledWith(code, {
ecmaFeatures: {},
jsx: false,
sourceType: 'script',
useJSXTextNode: true
});
expect(spyScope).toHaveBeenCalledWith(jasmine.any(Object), {
ecmaFeatures: {},
sourceType: 'script'
});
});

it('parseAndGenerateServices() should be called with options', () => {
const code = 'const valid = true;';
const spy = jest.spyOn(typescriptESTree, 'parseAndGenerateServices');
parseForESLint(code, {
loc: false,
comment: false,
range: false,
tokens: false,
sourceType: 'module',
ecmaVersion: 10,
ecmaFeatures: {
globalReturn: false,
jsx: false
},
// ts-estree specific
filePath: 'test/foo',
project: 'tsconfig.json',
useJSXTextNode: false,
errorOnUnknownASTType: false,
errorOnTypeScriptSyntacticAndSemanticIssues: false,
tsconfigRootDir: '../../',
extraFileExtensions: ['foo']
});
expect(spy).toHaveBeenCalledWith(code, {
jsx: false,
loc: false,
comment: false,
range: false,
tokens: false,
sourceType: 'module',
ecmaVersion: 10,
ecmaFeatures: {
globalReturn: false,
jsx: false
},
// ts-estree specific
filePath: 'test/foo',
project: 'tsconfig.json',
useJSXTextNode: false,
errorOnUnknownASTType: false,
errorOnTypeScriptSyntacticAndSemanticIssues: false,
tsconfigRootDir: '../../',
extraFileExtensions: ['foo']
});
});

it('Syntax should contain a frozen object of typescriptESTree.AST_NODE_TYPES', () => {
Expand Down
16 changes: 12 additions & 4 deletions packages/parser/tests/lib/tsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ describe('TSX', () => {
const code = fs.readFileSync(filename, 'utf8');
const config = {
useJSXTextNode: true,
jsx: true
ecmaFeatures: {
jsx: true
}
};
it(
testUtils.formatSnapshotName(filename, FIXTURES_DIR, '.tsx'),
Expand Down Expand Up @@ -53,7 +55,9 @@ describe('TSX', () => {
const config = {
parser: '@typescript-eslint/parser',
parserOptions: {
jsx: true
ecmaFeatures: {
jsx: true
}
}
};
const messages = linter.verify(code, config);
Expand Down Expand Up @@ -85,7 +89,9 @@ describe('TSX', () => {
const config = {
parser: '@typescript-eslint/parser',
parserOptions: {
jsx: true
ecmaFeatures: {
jsx: true
}
}
};
const messages = linter.verify(code, config, { filename: 'test.ts' });
Expand Down Expand Up @@ -117,7 +123,9 @@ describe('TSX', () => {
const config = {
parser: '@typescript-eslint/parser',
parserOptions: {
jsx: false
ecmaFeatures: {
jsx: false
}
}
};
const messages = linter.verify(code, config, { filename: 'test.tsx' });
Expand Down
8 changes: 4 additions & 4 deletions packages/typescript-estree/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,10 +350,7 @@ function generateAST<T extends ParserOptions = ParserOptions>(
// Public
//------------------------------------------------------------------------------

export { AST_NODE_TYPES } from './ast-node-types';
export { version };

const version = packageJSON.version;
export const version: string = packageJSON.version;

export function parse<T extends ParserOptions = ParserOptions>(
code: string,
Expand All @@ -378,3 +375,6 @@ export function parseAndGenerateServices(code: string, options: ParserOptions) {
}
};
}

export { AST_NODE_TYPES } from './ast-node-types';
export { ParserOptions };