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

Commit 3be357e

Browse files
uniqueiniquityJamesHenry
authored andcommitted
fix: non-existent files and custom file extensions (#53)
1 parent 593b779 commit 3be357e

File tree

5 files changed

+184
-45
lines changed

5 files changed

+184
-45
lines changed

src/parser.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
* @copyright jQuery Foundation and other contributors, https://jquery.org/
66
* MIT License
77
*/
8-
import calculateProjectParserOptions from './tsconfig-parser';
8+
import {
9+
calculateProjectParserOptions,
10+
createProgram
11+
} from './tsconfig-parser';
912
import semver from 'semver';
1013
import ts from 'typescript';
1114
import convert from './ast-converter';
@@ -57,7 +60,8 @@ function resetExtra(): void {
5760
projects: [],
5861
errorOnUnknownASTType: false,
5962
code: '',
60-
tsconfigRootDir: process.cwd()
63+
tsconfigRootDir: process.cwd(),
64+
extraFileExtensions: []
6165
};
6266
}
6367

@@ -73,7 +77,7 @@ function getASTFromProject(code: string, options: ParserOptions) {
7377
options.filePath || getFileName(options),
7478
extra
7579
),
76-
(currentProgram: ts.Program) => {
80+
currentProgram => {
7781
const ast = currentProgram.getSourceFile(
7882
options.filePath || getFileName(options)
7983
);
@@ -82,6 +86,18 @@ function getASTFromProject(code: string, options: ParserOptions) {
8286
);
8387
}
8488

89+
/**
90+
* @param {string} code The code of the file being linted
91+
* @param {Object} options The config object
92+
* @returns {{ast: ts.SourceFile, program: ts.Program} | undefined} If found, returns the source file corresponding to the code and the containing program
93+
*/
94+
function getASTAndDefaultProject(code: string, options: ParserOptions) {
95+
const fileName = options.filePath || getFileName(options);
96+
const program = createProgram(code, fileName, extra);
97+
const ast = program && program.getSourceFile(fileName);
98+
return ast && { ast, program };
99+
}
100+
85101
/**
86102
* @param {string} code The code of the file being linted
87103
* @returns {{ast: ts.SourceFile, program: ts.Program}} Returns a new source file and program corresponding to the linted code
@@ -154,6 +170,7 @@ function getProgramAndAST(
154170
) {
155171
return (
156172
(shouldProvideParserServices && getASTFromProject(code, options)) ||
173+
(shouldProvideParserServices && getASTAndDefaultProject(code, options)) ||
157174
createNewProgram(code)
158175
);
159176
}
@@ -254,6 +271,13 @@ function generateAST<T extends ParserOptions = ParserOptions>(
254271
if (typeof options.tsconfigRootDir === 'string') {
255272
extra.tsconfigRootDir = options.tsconfigRootDir;
256273
}
274+
275+
if (
276+
Array.isArray(options.extraFileExtensions) &&
277+
options.extraFileExtensions.every(ext => typeof ext === 'string')
278+
) {
279+
extra.extraFileExtensions = options.extraFileExtensions;
280+
}
257281
}
258282

259283
if (!isRunningSupportedTypeScriptVersion && !warnedAboutTSVersion) {

src/temp-types-based-on-js-source.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export interface Extra {
7070
log: Function;
7171
projects: string[];
7272
tsconfigRootDir: string;
73+
extraFileExtensions: string[];
7374
}
7475

7576
export interface ParserOptions {
@@ -84,4 +85,5 @@ export interface ParserOptions {
8485
project?: string | string[];
8586
filePath?: string;
8687
tsconfigRootDir?: string;
88+
extraFileExtensions?: string[];
8789
}

src/tsconfig-parser.ts

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ import { Extra } from './temp-types-based-on-js-source';
88
// Environment calculation
99
//------------------------------------------------------------------------------
1010

11+
/**
12+
* Default compiler options for program generation from single root file
13+
* @type {ts.CompilerOptions}
14+
*/
15+
const defaultCompilerOptions: ts.CompilerOptions = {
16+
allowNonTsExtensions: true,
17+
allowJs: true
18+
};
19+
1120
/**
1221
* Maps tsconfig paths to their corresponding file contents and resulting watches
1322
* @type {Map<string, ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram>>}
@@ -54,7 +63,7 @@ const noopFileWatcher = { close: () => {} };
5463
* @param {string[]} extra.project Provided tsconfig paths
5564
* @returns {ts.Program[]} The programs corresponding to the supplied tsconfig paths
5665
*/
57-
export default function calculateProjectParserOptions(
66+
export function calculateProjectParserOptions(
5867
code: string,
5968
filePath: string,
6069
extra: Extra
@@ -90,7 +99,7 @@ export default function calculateProjectParserOptions(
9099
// create compiler host
91100
const watchCompilerHost = ts.createWatchCompilerHost(
92101
tsconfigPath,
93-
/*optionsToExtend*/ undefined,
102+
/*optionsToExtend*/ { allowNonTsExtensions: true } as ts.CompilerOptions,
94103
ts.sys,
95104
ts.createSemanticDiagnosticsBuilderProgram,
96105
diagnosticReporter,
@@ -136,6 +145,32 @@ export default function calculateProjectParserOptions(
136145
// ensure fileWatchers aren't created for directories
137146
watchCompilerHost.watchDirectory = () => noopFileWatcher;
138147

148+
// allow files with custom extensions to be included in program (uses internal ts api)
149+
const oldOnDirectoryStructureHostCreate = (watchCompilerHost as any)
150+
.onCachedDirectoryStructureHostCreate;
151+
(watchCompilerHost as any).onCachedDirectoryStructureHostCreate = (
152+
host: any
153+
) => {
154+
const oldReadDirectory = host.readDirectory;
155+
host.readDirectory = (
156+
path: string,
157+
extensions?: ReadonlyArray<string>,
158+
exclude?: ReadonlyArray<string>,
159+
include?: ReadonlyArray<string>,
160+
depth?: number
161+
) =>
162+
oldReadDirectory(
163+
path,
164+
!extensions
165+
? undefined
166+
: extensions.concat(extra.extraFileExtensions),
167+
exclude,
168+
include,
169+
depth
170+
);
171+
oldOnDirectoryStructureHostCreate(host);
172+
};
173+
139174
// create program
140175
const programWatch = ts.createWatchProgram(watchCompilerHost);
141176
const program = programWatch.getProgram().getProgram();
@@ -147,3 +182,43 @@ export default function calculateProjectParserOptions(
147182

148183
return results;
149184
}
185+
186+
/**
187+
* Create program from single root file. Requires a single tsconfig to be specified.
188+
* @param code The code being linted
189+
* @param filePath The file being linted
190+
* @param {string} extra.tsconfigRootDir The root directory for relative tsconfig paths
191+
* @param {string[]} extra.project Provided tsconfig paths
192+
* @returns {ts.Program} The program containing just the file being linted and associated library files
193+
*/
194+
export function createProgram(code: string, filePath: string, extra: Extra) {
195+
if (!extra.projects || extra.projects.length !== 1) {
196+
return undefined;
197+
}
198+
199+
let tsconfigPath = extra.projects[0];
200+
201+
// if absolute paths aren't provided, make relative to tsconfigRootDir
202+
if (!path.isAbsolute(tsconfigPath)) {
203+
tsconfigPath = path.join(extra.tsconfigRootDir, tsconfigPath);
204+
}
205+
206+
const commandLine = ts.getParsedCommandLineOfConfigFile(
207+
tsconfigPath,
208+
defaultCompilerOptions,
209+
{ ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} }
210+
);
211+
212+
if (!commandLine) {
213+
return undefined;
214+
}
215+
216+
const compilerHost = ts.createCompilerHost(commandLine.options);
217+
const oldReadFile = compilerHost.readFile;
218+
compilerHost.readFile = (fileName: string) =>
219+
path.normalize(fileName) === path.normalize(filePath)
220+
? code
221+
: oldReadFile(fileName);
222+
223+
return ts.createProgram([filePath], commandLine.options, compilerHost);
224+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
const x = [3, 4, 5];

tests/lib/semanticInfo.ts

Lines changed: 77 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -77,48 +77,17 @@ describe('semanticInfo', () => {
7777
createOptions(fileName)
7878
);
7979

80-
// get type checker
81-
expect(parseResult).toHaveProperty('services.program.getTypeChecker');
82-
const checker = parseResult.services.program!.getTypeChecker();
83-
84-
// get number node (ast shape validated by snapshot)
85-
const arrayMember = (parseResult.ast as any).body[0].declarations[0].init
86-
.elements[0];
87-
expect(parseResult).toHaveProperty('services.esTreeNodeToTSNodeMap');
88-
89-
// get corresponding TS node
90-
const tsArrayMember = parseResult.services.esTreeNodeToTSNodeMap!.get(
91-
arrayMember
92-
);
93-
expect(tsArrayMember).toBeDefined();
94-
expect(tsArrayMember.kind).toBe(ts.SyntaxKind.NumericLiteral);
95-
expect(tsArrayMember.text).toBe('3');
96-
97-
// get type of TS node
98-
const arrayMemberType: any = checker.getTypeAtLocation(tsArrayMember);
99-
expect(arrayMemberType.flags).toBe(ts.TypeFlags.NumberLiteral);
100-
expect(arrayMemberType.value).toBe(3);
101-
102-
// make sure it maps back to original ESTree node
103-
expect(parseResult).toHaveProperty('services.tsNodeToESTreeNodeMap');
104-
expect(parseResult.services.tsNodeToESTreeNodeMap!.get(tsArrayMember)).toBe(
105-
arrayMember
106-
);
107-
108-
// get bound name
109-
const boundName = (parseResult.ast as any).body[0].declarations[0].id;
110-
expect(boundName.name).toBe('x');
111-
112-
const tsBoundName = parseResult.services.esTreeNodeToTSNodeMap!.get(
113-
boundName
114-
);
115-
expect(tsBoundName).toBeDefined();
80+
testIsolatedFile(parseResult);
81+
});
11682

117-
checkNumberArrayType(checker, tsBoundName);
83+
test('isolated-vue-file tests', () => {
84+
const fileName = path.resolve(FIXTURES_DIR, 'extra-file-extension.vue');
85+
const parseResult = parseCodeAndGenerateServices(shelljs.cat(fileName), {
86+
...createOptions(fileName),
87+
extraFileExtensions: ['.vue']
88+
});
11889

119-
expect(parseResult.services.tsNodeToESTreeNodeMap!.get(tsBoundName)).toBe(
120-
boundName
121-
);
90+
testIsolatedFile(parseResult);
12291
});
12392

12493
test('imported-file tests', () => {
@@ -150,6 +119,32 @@ describe('semanticInfo', () => {
150119
).toBe(arrayBoundName);
151120
});
152121

122+
test('non-existent file tests', () => {
123+
const parseResult = parseCodeAndGenerateServices(
124+
`const x = [parseInt("5")];`,
125+
createOptions('<input>')
126+
);
127+
128+
// get type checker
129+
expect(parseResult).toHaveProperty('services.program.getTypeChecker');
130+
const checker = parseResult.services.program!.getTypeChecker();
131+
132+
// get bound name
133+
const boundName = (parseResult.ast as any).body[0].declarations[0].id;
134+
expect(boundName.name).toBe('x');
135+
136+
const tsBoundName = parseResult.services.esTreeNodeToTSNodeMap!.get(
137+
boundName
138+
);
139+
expect(tsBoundName).toBeDefined();
140+
141+
checkNumberArrayType(checker, tsBoundName);
142+
143+
expect(parseResult.services.tsNodeToESTreeNodeMap!.get(tsBoundName)).toBe(
144+
boundName
145+
);
146+
});
147+
153148
test('non-existent project file', () => {
154149
const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts');
155150
const badConfig = createOptions(fileName);
@@ -178,6 +173,48 @@ describe('semanticInfo', () => {
178173
});
179174
});
180175

176+
function testIsolatedFile(parseResult: any) {
177+
// get type checker
178+
expect(parseResult).toHaveProperty('services.program.getTypeChecker');
179+
const checker = parseResult.services.program!.getTypeChecker();
180+
181+
// get number node (ast shape validated by snapshot)
182+
const arrayMember = (parseResult.ast as any).body[0].declarations[0].init
183+
.elements[0];
184+
expect(parseResult).toHaveProperty('services.esTreeNodeToTSNodeMap');
185+
186+
// get corresponding TS node
187+
const tsArrayMember = parseResult.services.esTreeNodeToTSNodeMap!.get(
188+
arrayMember
189+
);
190+
expect(tsArrayMember).toBeDefined();
191+
expect(tsArrayMember.kind).toBe(ts.SyntaxKind.NumericLiteral);
192+
expect((tsArrayMember as ts.NumericLiteral).text).toBe('3');
193+
194+
// get type of TS node
195+
const arrayMemberType: any = checker.getTypeAtLocation(tsArrayMember);
196+
expect(arrayMemberType.flags).toBe(ts.TypeFlags.NumberLiteral);
197+
expect(arrayMemberType.value).toBe(3);
198+
199+
// make sure it maps back to original ESTree node
200+
expect(parseResult).toHaveProperty('services.tsNodeToESTreeNodeMap');
201+
expect(parseResult.services.tsNodeToESTreeNodeMap!.get(tsArrayMember)).toBe(
202+
arrayMember
203+
);
204+
205+
// get bound name
206+
const boundName = (parseResult.ast as any).body[0].declarations[0].id;
207+
expect(boundName.name).toBe('x');
208+
const tsBoundName = parseResult.services.esTreeNodeToTSNodeMap!.get(
209+
boundName
210+
);
211+
expect(tsBoundName).toBeDefined();
212+
checkNumberArrayType(checker, tsBoundName);
213+
expect(parseResult.services.tsNodeToESTreeNodeMap!.get(tsBoundName)).toBe(
214+
boundName
215+
);
216+
}
217+
181218
/**
182219
* Verifies that the type of a TS node is number[] as expected
183220
* @param {ts.TypeChecker} checker

0 commit comments

Comments
 (0)