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

Analyze TypeScript code inside Vue.js single file components #2471

Merged
merged 2 commits into from Feb 2, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 0 additions & 2 deletions eslint-bridge/package.json
Expand Up @@ -33,7 +33,6 @@
"@types/express": "4.16.0",
"@types/jest": "26.0.14",
"@types/node": "14.11.5",
"@types/semver": "7.3.1",
"fs-extra": "7.0.0",
"jest": "26.6.3",
"jest-sonar-reporter": "1.3.0",
Expand All @@ -56,7 +55,6 @@
"espree": "6.1.2",
"express": "4.16.3",
"run-node": "1.0.0",
"semver": "7.3.2",
"typescript": "3.8.3",
"vue-eslint-parser": "7.1.0"
},
Expand Down
10 changes: 5 additions & 5 deletions eslint-bridge/src/analyzer.ts
Expand Up @@ -110,14 +110,14 @@ export interface IssueLocation {
}

export function analyzeJavaScript(input: AnalysisInput): AnalysisResponse {
return analyze(
input,
input.filePath.endsWith('.vue') ? parseVueSourceFile : parseJavaScriptSourceFile,
);
return analyze(input, parseJavaScriptSourceFile);
}

export function analyzeTypeScript(input: AnalysisInput): AnalysisResponse {
return analyze(input, parseTypeScriptSourceFile);
return analyze(
input,
input.filePath.endsWith('.vue') ? parseVueSourceFile : parseTypeScriptSourceFile,
);
}

function getFileContent(filePath: string) {
Expand Down
76 changes: 25 additions & 51 deletions eslint-bridge/src/parser.ts
Expand Up @@ -22,18 +22,9 @@ import * as babel from 'babel-eslint';
import { Linter, SourceCode } from 'eslint';
import { ParsingError } from './analyzer';
import * as VueJS from 'vue-eslint-parser';
import * as semver from 'semver';
import { version as typescriptRuntimeVersion } from 'typescript';
import * as tsParser from '@typescript-eslint/parser';
import { getContext } from './context';

// this value is taken from typescript-estree
// still we might consider extending this range
// if everything which we need is working on older/newer versions
const TYPESCRIPT_MINIMUM_VERSION = '3.3.1';
// next released version is 4.0.0, we need version which is above current 3.9.x and below 4.0.0
const TYPESCRIPT_MAXIMUM_VERSION = '3.10.0';

export const PARSER_CONFIG_MODULE: Linter.ParserOptions = {
tokens: true,
comment: true,
Expand Down Expand Up @@ -100,20 +91,12 @@ export function parseJavaScriptSourceFile(
};
}

let typescriptVersionLogged = false;

export function parseTypeScriptSourceFile(
fileContent: string,
filePath: string,
tsConfigs?: string[],
): SourceCode | ParsingError {
try {
if (!typescriptVersionLogged) {
console.log(`Version of TypeScript used during analysis: ${typescriptRuntimeVersion}`);
typescriptVersionLogged = true;
}

checkTypeScriptVersionCompatibility(typescriptRuntimeVersion);
const result = tsParser.parseForESLint(fileContent, {
...PARSER_CONFIG_MODULE,
filePath,
Expand All @@ -133,48 +116,39 @@ export function parseTypeScriptSourceFile(
}
}

let reportedNewerTypeScriptVersion = false;

export function resetReportedNewerTypeScriptVersion() {
reportedNewerTypeScriptVersion = false;
}

// exported for testing
export function checkTypeScriptVersionCompatibility(currentVersion: string) {
if (semver.gt(currentVersion, TYPESCRIPT_MAXIMUM_VERSION) && !reportedNewerTypeScriptVersion) {
reportedNewerTypeScriptVersion = true;
console.log(
`WARN You are using version of TypeScript ${currentVersion} which is not officially supported; ` +
`supported versions >=${TYPESCRIPT_MINIMUM_VERSION} <${TYPESCRIPT_MAXIMUM_VERSION}`,
);
} else if (semver.lt(currentVersion, TYPESCRIPT_MINIMUM_VERSION)) {
throw {
message: `You are using version of TypeScript ${currentVersion} which is not supported; supported versions >=${TYPESCRIPT_MINIMUM_VERSION}`,
};
}
}

export function unloadTypeScriptEslint() {
tsParser.clearCaches();
}

export function parseVueSourceFile(fileContent: string): SourceCode | ParsingError {
let exceptionToReport: ParseException | null = null;
// setting parser to be able to parse more code (by default `espree` is used by vue parser)
const vueModuleConfig = { ...PARSER_CONFIG_MODULE, parser: 'babel-eslint' };
for (const config of [vueModuleConfig, PARSER_CONFIG_SCRIPT]) {
export function parseVueSourceFile(
fileContent: string,
filePath: string,
tsConfigs?: string[],
): SourceCode | ParsingError {
let exception: ParseException | null = null;
const parsers = ['@typescript-eslint/parser', 'espree', 'babel-eslint'];
for (const parser of parsers) {
try {
const result = VueJS.parseForESLint(fileContent, config);
return new SourceCode(fileContent, result.ast as any);
} catch (exception) {
exceptionToReport = exception;
const result = VueJS.parseForESLint(fileContent, {
filePath,
parser,
project: tsConfigs,
extraFileExtensions: ['.vue'],
...PARSER_CONFIG_MODULE,
});
return new SourceCode(({
...result,
parserServices: result.services,
text: fileContent,
} as unknown) as SourceCode.Config);
} catch (err) {
exception = err as ParseException;
}
}
// if we reach this point, we are sure that "exceptionToReport" is defined
return {
line: exceptionToReport!.lineNumber,
message: exceptionToReport!.message,
code: ParseExceptionCode.Parsing,
line: exception!.lineNumber,
message: exception!.message,
code: parseExceptionCodeOf(exception!.message),
};
}

Expand Down
30 changes: 16 additions & 14 deletions eslint-bridge/tests/analyzer.test.ts
Expand Up @@ -124,20 +124,6 @@ describe('#analyzeJavaScript', () => {
expect(issues).toContainEqual(noDuplicateStringIssue);
});

it('should analyze Vue.js file', () => {
const filePath = join(__dirname, './fixtures/vue-project/sample.lint.vue');
const fileContent = fs.readFileSync(filePath, { encoding: 'utf8' });
initLinter([
{ key: 'no-one-iteration-loop', configurations: [] },
{ key: 'no-duplicate-string', configurations: ['2'] },
]);
const { issues } = analyzeJavaScript({
filePath: filePath,
fileContent: fileContent,
});
expect(issues).toHaveLength(2);
});

it('should handle BOM', () => {
const filePath = join(__dirname, './fixtures/js-project/fileWithBom.lint.js');

Expand Down Expand Up @@ -348,4 +334,20 @@ describe('#analyzeTypeScript', () => {
);
jest.resetAllMocks();
});

it('should analyze Vue.js file', () => {
const filePath = join(__dirname, './fixtures/vue-project/sample.lint.vue');
const tsConfig = join(__dirname, './fixtures/vue-project/tsconfig.json');
const fileContent = fs.readFileSync(filePath, { encoding: 'utf8' });
initLinter([
{ key: 'no-extra-semi', configurations: [] },
{ key: 'no-return-type-any', configurations: [] },
]);
const { issues } = analyzeTypeScript({
filePath,
fileContent,
tsConfigs: [tsConfig],
});
expect(issues).toHaveLength(2);
});
});
20 changes: 10 additions & 10 deletions eslint-bridge/tests/fixtures/vue-project/sample.lint.vue
@@ -1,17 +1,17 @@
<template>
<p>Hello</p>
<p>{{background}}</p>
</template>

<script>
module.exports = {
foo: function () {
for (var i = 0; i < 10; i++) {
console.log("i is " + i);
break;
}
foo("Hello, world"); foo("Hello, world");
}
<script lang="ts">
import Vue from 'vue';

function hello(name: string): any {
return `Hello, ${name}`;;
}

export default Vue.extend({
greet: hello,
});
</script>

<style scoped>
Expand Down
3 changes: 3 additions & 0 deletions eslint-bridge/tests/fixtures/vue-project/tsconfig.json
@@ -0,0 +1,3 @@
{
"files": ["sample.lint.vue"]
}
93 changes: 40 additions & 53 deletions eslint-bridge/tests/parser.test.ts
Expand Up @@ -25,8 +25,6 @@ import {
parseJavaScriptSourceFile,
parseTypeScriptSourceFile,
parseVueSourceFile,
checkTypeScriptVersionCompatibility,
resetReportedNewerTypeScriptVersion,
ParseExceptionCode,
parseExceptionCodeOf,
} from '../src/parser';
Expand All @@ -35,24 +33,9 @@ import { SourceCode } from 'eslint';
import { ParsingError } from '../src/analyzer';
import visit from '../src/utils/visitor';
import * as path from 'path';
import * as ts from 'typescript';
import * as fs from 'fs';
import { setContext } from '../src/context';

describe('TypeScript version', () => {
beforeEach(() => {
console.log = jest.fn();
});

it('should log typescript version once', () => {
parseTypeScriptSourceFile('', 'foo.ts');
parseTypeScriptSourceFile('', 'foo.ts');
const callsToLogger = (console.log as jest.Mock).mock.calls;
const message = `Version of TypeScript used during analysis: ${ts.version}`;
expect(callsToLogger.filter(args => args[0] === message)).toHaveLength(1);
});
});

describe('parseJavaScriptSourceFile', () => {
beforeEach(() => {
console.error = jest.fn();
Expand Down Expand Up @@ -227,35 +210,6 @@ describe('parseTypeScriptSourceFile', () => {
);
});

it('should throw a parsing exception with TypeScript version below minimum expected', () => {
let parsingException = undefined;
try {
checkTypeScriptVersionCompatibility('1.2.3');
} catch (exception) {
parsingException = exception;
}
expect(parsingException).toBeDefined;
expect(parsingException).toEqual({
message:
'You are using version of TypeScript 1.2.3 which is not supported; supported versions >=3.3.1',
});
});

it('should log a warning with TypeScript version above maximum expected', () => {
console.log = jest.fn();
resetReportedNewerTypeScriptVersion();
checkTypeScriptVersionCompatibility('5.0.0');
expect(console.log).toHaveBeenCalledWith(
'WARN You are using version of TypeScript 5.0.0 which is not officially supported; supported versions >=3.3.1 <3.10.0',
);
console.log = jest.fn();
checkTypeScriptVersionCompatibility('5.0.0');
// should log only once
expect(console.log).not.toHaveBeenCalled();

jest.resetAllMocks();
});

it('should return correct parsing exception code from exception message', () => {
expect(parseExceptionCodeOf("Cannot find module 'typescript'")).toEqual(
ParseExceptionCode.MissingTypeScript,
Expand All @@ -271,6 +225,10 @@ describe('parseTypeScriptSourceFile', () => {
});

describe('parseVueSourceFile', () => {
const dirName = __dirname + '/fixtures/vue-project';
const filePath = dirName + '/sample.lint.vue';
const tsConfig = dirName + '/tsconfig.json';

it('should parse Vue.js syntax', () => {
const code = `
module.exports = {
Expand All @@ -282,7 +240,8 @@ describe('parseVueSourceFile', () => {
}`;

const parsedJS = parseJavaScriptSourceFile(code, 'foo.js') as SourceCode;
const parsedVueJS = parseVueSourceFile(`
const parsedVueJS = parseVueSourceFile(
`
<template>
<p>{{foo}}</p>
</template>
Expand All @@ -292,7 +251,10 @@ describe('parseVueSourceFile', () => {
<style>
p { text-align: center; }
</style>
`) as SourceCode;
`,
filePath,
[tsConfig],
) as SourceCode;

const expected = [],
actual = [];
Expand All @@ -302,15 +264,33 @@ describe('parseVueSourceFile', () => {
});

it('should log parse error with Vue.js', () => {
const parsingError = parseVueSourceFile(`
const parsingError = parseVueSourceFile(
`
<script>
module.exports = {
</script>`) as ParsingError;
</script>`,
filePath,
[tsConfig],
) as ParsingError;
expect(parsingError).toBeDefined();
expect(parsingError.line).toEqual(4);
expect(parsingError.message).toEqual('Unexpected token');
expect(parsingError.message).toEqual('Unexpected token (3:4)');
expect(parsingError.code).toEqual(ParseExceptionCode.Parsing);
});

it('should parse TypeScript syntax', () => {
const fileContent = `
<template></template>
<script lang="ts">
type alias = string | string[]
let union: string | null | undefined;
let assertion = something as number;
</script>
<style></style>`;
const sourceCode = parseVueSourceFile(fileContent, filePath, [tsConfig]);
expect(sourceCode).toBeDefined();
expect(sourceCode).toBeInstanceOf(SourceCode);
});
});

describe('parse import expression', () => {
Expand All @@ -320,10 +300,17 @@ describe('parse import expression', () => {
});

it('should parse Vue.js with import expression', () => {
const sourceCode = parseVueSourceFile(`
const dirName = __dirname + '/fixtures/vue-project';
const filePath = dirName + '/sample.lint.vue';
const tsConfig = dirName + '/tsconfig.json';
const sourceCode = parseVueSourceFile(
`
<script>
import("moduleName");
</script>`) as SourceCode;
</script>`,
filePath,
[tsConfig],
) as SourceCode;
expect(sourceCode).toBeDefined();
expect(sourceCode).toBeInstanceOf(SourceCode);
expect(sourceCode.visitorKeys['ImportExpression']).toBeDefined();
Expand Down