Skip to content
This repository was archived by the owner on Jan 14, 2019. It is now read-only.

Handle non-existent files and custom file extensions #53

Merged
merged 8 commits into from
Dec 27, 2018
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
30 changes: 27 additions & 3 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
* @copyright jQuery Foundation and other contributors, https://jquery.org/
* MIT License
*/
import calculateProjectParserOptions from './tsconfig-parser';
import {
calculateProjectParserOptions,
createProgram
} from './tsconfig-parser';
import semver from 'semver';
import ts from 'typescript';
import convert from './ast-converter';
Expand Down Expand Up @@ -57,7 +60,8 @@ function resetExtra(): void {
projects: [],
errorOnUnknownASTType: false,
code: '',
tsconfigRootDir: process.cwd()
tsconfigRootDir: process.cwd(),
extraFileExtensions: []
};
}

Expand All @@ -73,7 +77,7 @@ function getASTFromProject(code: string, options: ParserOptions) {
options.filePath || getFileName(options),
extra
),
(currentProgram: ts.Program) => {
currentProgram => {
const ast = currentProgram.getSourceFile(
options.filePath || getFileName(options)
);
Expand All @@ -82,6 +86,18 @@ function getASTFromProject(code: string, options: ParserOptions) {
);
}

/**
* @param {string} code The code of the file being linted
* @param {Object} options The config object
* @returns {{ast: ts.SourceFile, program: ts.Program} | undefined} If found, returns the source file corresponding to the code and the containing program
*/
function getASTAndDefaultProject(code: string, options: ParserOptions) {
const fileName = options.filePath || getFileName(options);
const program = createProgram(code, fileName, extra);
const ast = program && program.getSourceFile(fileName);
return ast && { ast, program };
}

/**
* @param {string} code The code of the file being linted
* @returns {{ast: ts.SourceFile, program: ts.Program}} Returns a new source file and program corresponding to the linted code
Expand Down Expand Up @@ -154,6 +170,7 @@ function getProgramAndAST(
) {
return (
(shouldProvideParserServices && getASTFromProject(code, options)) ||
(shouldProvideParserServices && getASTAndDefaultProject(code, options)) ||
createNewProgram(code)
);
}
Expand Down Expand Up @@ -254,6 +271,13 @@ function generateAST<T extends ParserOptions = ParserOptions>(
if (typeof options.tsconfigRootDir === 'string') {
extra.tsconfigRootDir = options.tsconfigRootDir;
}

if (
Array.isArray(options.extraFileExtensions) &&
options.extraFileExtensions.every(ext => typeof ext === 'string')
) {
extra.extraFileExtensions = options.extraFileExtensions;
}
}

if (!isRunningSupportedTypeScriptVersion && !warnedAboutTSVersion) {
Expand Down
2 changes: 2 additions & 0 deletions src/temp-types-based-on-js-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export interface Extra {
log: Function;
projects: string[];
tsconfigRootDir: string;
extraFileExtensions: string[];
}

export interface ParserOptions {
Expand All @@ -84,4 +85,5 @@ export interface ParserOptions {
project?: string | string[];
filePath?: string;
tsconfigRootDir?: string;
extraFileExtensions?: string[];
}
79 changes: 77 additions & 2 deletions src/tsconfig-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ import { Extra } from './temp-types-based-on-js-source';
// Environment calculation
//------------------------------------------------------------------------------

/**
* Default compiler options for program generation from single root file
* @type {ts.CompilerOptions}
*/
const defaultCompilerOptions: ts.CompilerOptions = {
allowNonTsExtensions: true,
allowJs: true
};

/**
* Maps tsconfig paths to their corresponding file contents and resulting watches
* @type {Map<string, ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram>>}
Expand Down Expand Up @@ -54,7 +63,7 @@ const noopFileWatcher = { close: () => {} };
* @param {string[]} extra.project Provided tsconfig paths
* @returns {ts.Program[]} The programs corresponding to the supplied tsconfig paths
*/
export default function calculateProjectParserOptions(
export function calculateProjectParserOptions(
code: string,
filePath: string,
extra: Extra
Expand Down Expand Up @@ -90,7 +99,7 @@ export default function calculateProjectParserOptions(
// create compiler host
const watchCompilerHost = ts.createWatchCompilerHost(
tsconfigPath,
/*optionsToExtend*/ undefined,
/*optionsToExtend*/ { allowNonTsExtensions: true } as ts.CompilerOptions,
ts.sys,
ts.createSemanticDiagnosticsBuilderProgram,
diagnosticReporter,
Expand Down Expand Up @@ -136,6 +145,32 @@ export default function calculateProjectParserOptions(
// ensure fileWatchers aren't created for directories
watchCompilerHost.watchDirectory = () => noopFileWatcher;

// allow files with custom extensions to be included in program (uses internal ts api)
const oldOnDirectoryStructureHostCreate = (watchCompilerHost as any)
.onCachedDirectoryStructureHostCreate;
(watchCompilerHost as any).onCachedDirectoryStructureHostCreate = (
host: any
) => {
const oldReadDirectory = host.readDirectory;
host.readDirectory = (
path: string,
extensions?: ReadonlyArray<string>,
exclude?: ReadonlyArray<string>,
include?: ReadonlyArray<string>,
depth?: number
) =>
oldReadDirectory(
path,
!extensions
? undefined
: extensions.concat(extra.extraFileExtensions),
exclude,
include,
depth
);
oldOnDirectoryStructureHostCreate(host);
};

// create program
const programWatch = ts.createWatchProgram(watchCompilerHost);
const program = programWatch.getProgram().getProgram();
Expand All @@ -147,3 +182,43 @@ export default function calculateProjectParserOptions(

return results;
}

/**
* Create program from single root file. Requires a single tsconfig to be specified.
* @param code The code being linted
* @param filePath The file being linted
* @param {string} extra.tsconfigRootDir The root directory for relative tsconfig paths
* @param {string[]} extra.project Provided tsconfig paths
* @returns {ts.Program} The program containing just the file being linted and associated library files
*/
export function createProgram(code: string, filePath: string, extra: Extra) {
if (!extra.projects || extra.projects.length !== 1) {
return undefined;
}

let tsconfigPath = extra.projects[0];

// if absolute paths aren't provided, make relative to tsconfigRootDir
if (!path.isAbsolute(tsconfigPath)) {
tsconfigPath = path.join(extra.tsconfigRootDir, tsconfigPath);
}

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

if (!commandLine) {
return undefined;
}

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

return ts.createProgram([filePath], commandLine.options, compilerHost);
}
1 change: 1 addition & 0 deletions tests/fixtures/semanticInfo/extra-file-extension.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const x = [3, 4, 5];
117 changes: 77 additions & 40 deletions tests/lib/semanticInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,48 +77,17 @@ describe('semanticInfo', () => {
createOptions(fileName)
);

// get type checker
expect(parseResult).toHaveProperty('services.program.getTypeChecker');
const checker = parseResult.services.program!.getTypeChecker();

// get number node (ast shape validated by snapshot)
const arrayMember = (parseResult.ast as any).body[0].declarations[0].init
.elements[0];
expect(parseResult).toHaveProperty('services.esTreeNodeToTSNodeMap');

// get corresponding TS node
const tsArrayMember = parseResult.services.esTreeNodeToTSNodeMap!.get(
arrayMember
);
expect(tsArrayMember).toBeDefined();
expect(tsArrayMember.kind).toBe(ts.SyntaxKind.NumericLiteral);
expect(tsArrayMember.text).toBe('3');

// get type of TS node
const arrayMemberType: any = checker.getTypeAtLocation(tsArrayMember);
expect(arrayMemberType.flags).toBe(ts.TypeFlags.NumberLiteral);
expect(arrayMemberType.value).toBe(3);

// make sure it maps back to original ESTree node
expect(parseResult).toHaveProperty('services.tsNodeToESTreeNodeMap');
expect(parseResult.services.tsNodeToESTreeNodeMap!.get(tsArrayMember)).toBe(
arrayMember
);

// get bound name
const boundName = (parseResult.ast as any).body[0].declarations[0].id;
expect(boundName.name).toBe('x');

const tsBoundName = parseResult.services.esTreeNodeToTSNodeMap!.get(
boundName
);
expect(tsBoundName).toBeDefined();
testIsolatedFile(parseResult);
});

checkNumberArrayType(checker, tsBoundName);
test('isolated-vue-file tests', () => {
const fileName = path.resolve(FIXTURES_DIR, 'extra-file-extension.vue');
const parseResult = parseCodeAndGenerateServices(shelljs.cat(fileName), {
...createOptions(fileName),
extraFileExtensions: ['.vue']
});

expect(parseResult.services.tsNodeToESTreeNodeMap!.get(tsBoundName)).toBe(
boundName
);
testIsolatedFile(parseResult);
});

test('imported-file tests', () => {
Expand Down Expand Up @@ -150,6 +119,32 @@ describe('semanticInfo', () => {
).toBe(arrayBoundName);
});

test('non-existent file tests', () => {
const parseResult = parseCodeAndGenerateServices(
`const x = [parseInt("5")];`,
createOptions('<input>')
);

// get type checker
expect(parseResult).toHaveProperty('services.program.getTypeChecker');
const checker = parseResult.services.program!.getTypeChecker();

// get bound name
const boundName = (parseResult.ast as any).body[0].declarations[0].id;
expect(boundName.name).toBe('x');

const tsBoundName = parseResult.services.esTreeNodeToTSNodeMap!.get(
boundName
);
expect(tsBoundName).toBeDefined();

checkNumberArrayType(checker, tsBoundName);

expect(parseResult.services.tsNodeToESTreeNodeMap!.get(tsBoundName)).toBe(
boundName
);
});

test('non-existent project file', () => {
const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts');
const badConfig = createOptions(fileName);
Expand Down Expand Up @@ -178,6 +173,48 @@ describe('semanticInfo', () => {
});
});

function testIsolatedFile(parseResult: any) {
// get type checker
expect(parseResult).toHaveProperty('services.program.getTypeChecker');
const checker = parseResult.services.program!.getTypeChecker();

// get number node (ast shape validated by snapshot)
const arrayMember = (parseResult.ast as any).body[0].declarations[0].init
.elements[0];
expect(parseResult).toHaveProperty('services.esTreeNodeToTSNodeMap');

// get corresponding TS node
const tsArrayMember = parseResult.services.esTreeNodeToTSNodeMap!.get(
arrayMember
);
expect(tsArrayMember).toBeDefined();
expect(tsArrayMember.kind).toBe(ts.SyntaxKind.NumericLiteral);
expect((tsArrayMember as ts.NumericLiteral).text).toBe('3');

// get type of TS node
const arrayMemberType: any = checker.getTypeAtLocation(tsArrayMember);
expect(arrayMemberType.flags).toBe(ts.TypeFlags.NumberLiteral);
expect(arrayMemberType.value).toBe(3);

// make sure it maps back to original ESTree node
expect(parseResult).toHaveProperty('services.tsNodeToESTreeNodeMap');
expect(parseResult.services.tsNodeToESTreeNodeMap!.get(tsArrayMember)).toBe(
arrayMember
);

// get bound name
const boundName = (parseResult.ast as any).body[0].declarations[0].id;
expect(boundName.name).toBe('x');
const tsBoundName = parseResult.services.esTreeNodeToTSNodeMap!.get(
boundName
);
expect(tsBoundName).toBeDefined();
checkNumberArrayType(checker, tsBoundName);
expect(parseResult.services.tsNodeToESTreeNodeMap!.get(tsBoundName)).toBe(
boundName
);
}

/**
* Verifies that the type of a TS node is number[] as expected
* @param {ts.TypeChecker} checker
Expand Down