Skip to content

feat(typescript-estree): support long running lint without watch #1106

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 5 commits into from
Oct 19, 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
16 changes: 16 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@
"sourceMaps": true,
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"type": "node",
"request": "launch",
"name": "Run currently opened parser test",
"cwd": "${workspaceFolder}/packages/parser/",
"program": "${workspaceFolder}/node_modules/jest/bin/jest.js",
"args": [
"--runInBand",
"--no-cache",
"--no-coverage",
"${relativeFile}"
],
"sourceMaps": true,
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
43 changes: 25 additions & 18 deletions packages/eslint-plugin/tests/RuleTester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,30 @@ type RuleTesterConfig = Omit<TSESLint.RuleTesterConfig, 'parser'> & {
parser: typeof parser;
};
class RuleTester extends TSESLint.RuleTester {
private filename: string | undefined = undefined;

// as of eslint 6 you have to provide an absolute path to the parser
// but that's not as clean to type, this saves us trying to manually enforce
// that contributors require.resolve everything
constructor(options: RuleTesterConfig) {
constructor(private readonly options: RuleTesterConfig) {
super({
...options,
parser: require.resolve(options.parser),
});
}
private getFilename(options?: TSESLint.ParserOptions): string {
if (options) {
const filename = `file.ts${
options.ecmaFeatures && options.ecmaFeatures.jsx ? 'x' : ''
}`;
if (options.project) {
return path.join(getFixturesRootDir(), filename);
}

if (options.parserOptions && options.parserOptions.project) {
this.filename = path.join(getFixturesRootDir(), 'file.ts');
return filename;
} else if (this.options.parserOptions) {
return this.getFilename(this.options.parserOptions);
}

return 'file.ts';
}

// as of eslint 6 you have to provide an absolute path to the parser
Expand All @@ -34,25 +44,22 @@ class RuleTester extends TSESLint.RuleTester {
): void {
const errorMessage = `Do not set the parser at the test level unless you want to use a parser other than ${parser}`;

if (this.filename) {
tests.valid = tests.valid.map(test => {
if (typeof test === 'string') {
return {
code: test,
filename: this.filename,
};
}
return test;
});
}
tests.valid = tests.valid.map(test => {
if (typeof test === 'string') {
return {
code: test,
};
}
return test;
});

tests.valid.forEach(test => {
if (typeof test !== 'string') {
if (test.parser === parser) {
throw new Error(errorMessage);
}
if (!test.filename) {
test.filename = this.filename;
test.filename = this.getFilename(test.parserOptions);
}
}
});
Expand All @@ -61,7 +68,7 @@ class RuleTester extends TSESLint.RuleTester {
throw new Error(errorMessage);
}
if (!test.filename) {
test.filename = this.filename;
test.filename = this.getFilename(test.parserOptions);
}
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import debug from 'debug';
import path from 'path';
import ts from 'typescript';
import { Extra } from '../parser-options';
import {
getTsconfigPath,
DEFAULT_COMPILER_OPTIONS,
ASTAndProgram,
} from './shared';

const log = debug('typescript-eslint:typescript-estree:createDefaultProgram');

/**
* @param code The code of the file being linted
* @param options The config object
* @param extra.tsconfigRootDir The root directory for relative tsconfig paths
* @param extra.projects Provided tsconfig paths
* @returns If found, returns the source file corresponding to the code and the containing program
*/
function createDefaultProgram(
code: string,
extra: Extra,
): ASTAndProgram | undefined {
log('Getting default program for: %s', extra.filePath || 'unnamed file');

if (!extra.projects || extra.projects.length !== 1) {
return undefined;
}

const tsconfigPath = getTsconfigPath(extra.projects[0], extra);

const commandLine = ts.getParsedCommandLineOfConfigFile(
tsconfigPath,
DEFAULT_COMPILER_OPTIONS,
{ ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} },
);

if (!commandLine) {
return undefined;
}

const compilerHost = ts.createCompilerHost(commandLine.options, true);
const oldReadFile = compilerHost.readFile;
compilerHost.readFile = (fileName: string): string | undefined =>
path.normalize(fileName) === path.normalize(extra.filePath)
? code
: oldReadFile(fileName);

const program = ts.createProgram(
[extra.filePath],
commandLine.options,
compilerHost,
);
const ast = program.getSourceFile(extra.filePath);

return ast && { ast, program };
}

export { createDefaultProgram };
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import debug from 'debug';
import ts from 'typescript';
import { Extra } from '../parser-options';
import { ASTAndProgram, DEFAULT_COMPILER_OPTIONS } from './shared';

const log = debug('typescript-eslint:typescript-estree:createIsolatedProgram');

/**
* @param code The code of the file being linted
* @returns Returns a new source file and program corresponding to the linted code
*/
function createIsolatedProgram(code: string, extra: Extra): ASTAndProgram {
log('Getting isolated program for: %s', extra.filePath);

const compilerHost: ts.CompilerHost = {
fileExists() {
return true;
},
getCanonicalFileName() {
return extra.filePath;
},
getCurrentDirectory() {
return '';
},
getDirectories() {
return [];
},
getDefaultLibFileName() {
return 'lib.d.ts';
},

// TODO: Support Windows CRLF
getNewLine() {
return '\n';
},
getSourceFile(filename: string) {
return ts.createSourceFile(filename, code, ts.ScriptTarget.Latest, true);
},
readFile() {
return undefined;
},
useCaseSensitiveFileNames() {
return true;
},
writeFile() {
return null;
},
};

const program = ts.createProgram(
[extra.filePath],
{
noResolve: true,
target: ts.ScriptTarget.Latest,
jsx: extra.jsx ? ts.JsxEmit.Preserve : undefined,
...DEFAULT_COMPILER_OPTIONS,
},
compilerHost,
);

const ast = program.getSourceFile(extra.filePath);
if (!ast) {
throw new Error(
'Expected an ast to be returned for the single-file isolated program.',
);
}

return { ast, program };
}

export { createIsolatedProgram };
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import debug from 'debug';
import path from 'path';
import { getProgramsForProjects } from './createWatchProgram';
import { firstDefined } from '../node-utils';
import { Extra } from '../parser-options';
import { ASTAndProgram } from './shared';

const log = debug('typescript-eslint:typescript-estree:createProjectProgram');

/**
* @param code The code of the file being linted
* @param options The config object
* @returns If found, returns the source file corresponding to the code and the containing program
*/
function createProjectProgram(
code: string,
createDefaultProgram: boolean,
extra: Extra,
): ASTAndProgram | undefined {
log('Creating project program for: %s', extra.filePath);

const astAndProgram = firstDefined(
getProgramsForProjects(code, extra.filePath, extra),
currentProgram => {
const ast = currentProgram.getSourceFile(extra.filePath);
return ast && { ast, program: currentProgram };
},
);

if (!astAndProgram && !createDefaultProgram) {
// the file was either not matched within the tsconfig, or the extension wasn't expected
const errorLines = [
'"parserOptions.project" has been set for @typescript-eslint/parser.',
`The file does not match your project config: ${path.relative(
process.cwd(),
extra.filePath,
)}.`,
];
let hasMatchedAnError = false;

const fileExtension = path.extname(extra.filePath);
if (!['.ts', '.tsx', '.js', '.jsx'].includes(fileExtension)) {
const nonStandardExt = `The extension for the file (${fileExtension}) is non-standard`;
if (extra.extraFileExtensions && extra.extraFileExtensions.length > 0) {
if (!extra.extraFileExtensions.includes(fileExtension)) {
errorLines.push(
`${nonStandardExt}. It should be added to your existing "parserOptions.extraFileExtensions".`,
);
hasMatchedAnError = true;
}
} else {
errorLines.push(
`${nonStandardExt}. You should add "parserOptions.extraFileExtensions" to your config.`,
);
hasMatchedAnError = true;
}
}

if (!hasMatchedAnError) {
errorLines.push(
'The file must be included in at least one of the projects provided.',
);
hasMatchedAnError = true;
}

throw new Error(errorLines.join('\n'));
}

return astAndProgram;
}

export { createProjectProgram };
18 changes: 18 additions & 0 deletions packages/typescript-estree/src/create-program/createSourceFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import debug from 'debug';
import ts from 'typescript';
import { Extra } from '../parser-options';

const log = debug('typescript-eslint:typescript-estree:createIsolatedProgram');

function createSourceFile(code: string, extra: Extra): ts.SourceFile {
log('Getting AST without type information for: %s', extra.filePath);

return ts.createSourceFile(
extra.filePath,
code,
ts.ScriptTarget.Latest,
/* setParentNodes */ true,
);
}

export { createSourceFile };
Loading