Skip to content

Commit

Permalink
feat(typescript-estree): support long running lint without watch (#1106)
Browse files Browse the repository at this point in the history
  • Loading branch information
bradzacher committed Oct 19, 2019
1 parent 0c85ac3 commit ed5564d
Show file tree
Hide file tree
Showing 15 changed files with 882 additions and 700 deletions.
16 changes: 16 additions & 0 deletions .vscode/launch.json
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
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
@@ -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 };
@@ -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 };
@@ -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
@@ -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 };

0 comments on commit ed5564d

Please sign in to comment.