• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import debug from 'debug';
2import { sync as globSync } from 'globby';
3import isGlob from 'is-glob';
4import semver from 'semver';
5import * as ts from 'typescript';
6import { astConverter } from './ast-converter';
7import { convertError } from './convert';
8import { createDefaultProgram } from './create-program/createDefaultProgram';
9import { createIsolatedProgram } from './create-program/createIsolatedProgram';
10import { createProjectProgram } from './create-program/createProjectProgram';
11import { createSourceFile } from './create-program/createSourceFile';
12import { Extra, TSESTreeOptions, ParserServices } from './parser-options';
13import { getFirstSemanticOrSyntacticError } from './semantic-or-syntactic-errors';
14import { TSESTree } from './ts-estree';
15import { ASTAndProgram, ensureAbsolutePath } from './create-program/shared';
16
17const log = debug('typescript-eslint:typescript-estree:parser');
18
19/**
20 * This needs to be kept in sync with the top-level README.md in the
21 * typescript-eslint monorepo
22 */
23const SUPPORTED_TYPESCRIPT_VERSIONS = '>=3.3.1 <4.2.0';
24/*
25 * The semver package will ignore prerelease ranges, and we don't want to explicitly document every one
26 * List them all separately here, so we can automatically create the full string
27 */
28const SUPPORTED_PRERELEASE_RANGES: string[] = ['4.1.1-rc', '4.1.0-beta'];
29const ACTIVE_TYPESCRIPT_VERSION = ts.version;
30const isRunningSupportedTypeScriptVersion = semver.satisfies(
31  ACTIVE_TYPESCRIPT_VERSION,
32  [SUPPORTED_TYPESCRIPT_VERSIONS]
33    .concat(SUPPORTED_PRERELEASE_RANGES)
34    .join(' || '),
35);
36
37let extra: Extra;
38let warnedAboutTSVersion = false;
39
40function enforceString(code: unknown): string {
41  /**
42   * Ensure the source code is a string
43   */
44  if (typeof code !== 'string') {
45    return String(code);
46  }
47
48  return code;
49}
50
51/**
52 * @param code The code of the file being linted
53 * @param shouldProvideParserServices True if the program should be attempted to be calculated from provided tsconfig files
54 * @param shouldCreateDefaultProgram True if the program should be created from compiler host
55 * @returns Returns a source file and program corresponding to the linted code
56 */
57function getProgramAndAST(
58  code: string,
59  shouldProvideParserServices: boolean,
60  shouldCreateDefaultProgram: boolean,
61): ASTAndProgram {
62  return (
63    (shouldProvideParserServices &&
64      createProjectProgram(code, shouldCreateDefaultProgram, extra)) ||
65    (shouldProvideParserServices &&
66      shouldCreateDefaultProgram &&
67      createDefaultProgram(code, extra)) ||
68    createIsolatedProgram(code, extra)
69  );
70}
71
72/**
73 * Compute the filename based on the parser options.
74 *
75 * Even if jsx option is set in typescript compiler, filename still has to
76 * contain .tsx file extension.
77 *
78 * @param options Parser options
79 */
80function getFileName({ jsx }: { jsx?: boolean } = {}): string {
81  return jsx ? 'estree.tsx' : 'estree.ts';
82}
83
84/**
85 * Resets the extra config object
86 */
87function resetExtra(): void {
88  extra = {
89    code: '',
90    comment: false,
91    comments: [],
92    createDefaultProgram: false,
93    debugLevel: new Set(),
94    errorOnTypeScriptSyntacticAndSemanticIssues: false,
95    errorOnUnknownASTType: false,
96    EXPERIMENTAL_useSourceOfProjectReferenceRedirect: false,
97    extraFileExtensions: [],
98    filePath: getFileName(),
99    jsx: false,
100    loc: false,
101    log: console.log, // eslint-disable-line no-console
102    preserveNodeMaps: true,
103    projects: [],
104    range: false,
105    strict: false,
106    tokens: null,
107    tsconfigRootDir: process.cwd(),
108    useJSXTextNode: false,
109  };
110}
111
112/**
113 * Normalizes, sanitizes, resolves and filters the provided
114 */
115function prepareAndTransformProjects(
116  projectsInput: string | string[] | undefined,
117  ignoreListInput: string[],
118): string[] {
119  let projects: string[] = [];
120
121  // Normalize and sanitize the project paths
122  if (typeof projectsInput === 'string') {
123    projects.push(projectsInput);
124  } else if (Array.isArray(projectsInput)) {
125    for (const project of projectsInput) {
126      if (typeof project === 'string') {
127        projects.push(project);
128      }
129    }
130  }
131
132  if (projects.length === 0) {
133    return projects;
134  }
135
136  // Transform glob patterns into paths
137  const globbedProjects = projects.filter(project => isGlob(project));
138  projects = projects
139    .filter(project => !isGlob(project))
140    .concat(
141      globSync([...globbedProjects, ...ignoreListInput], {
142        cwd: extra.tsconfigRootDir,
143      }),
144    );
145
146  log(
147    'parserOptions.project (excluding ignored) matched projects: %s',
148    projects,
149  );
150
151  return projects;
152}
153
154function applyParserOptionsToExtra(options: TSESTreeOptions): void {
155  /**
156   * Configure Debug logging
157   */
158  if (options.debugLevel === true) {
159    extra.debugLevel = new Set(['typescript-eslint']);
160  } else if (Array.isArray(options.debugLevel)) {
161    extra.debugLevel = new Set(options.debugLevel);
162  }
163  if (extra.debugLevel.size > 0) {
164    // debug doesn't support multiple `enable` calls, so have to do it all at once
165    const namespaces = [];
166    if (extra.debugLevel.has('typescript-eslint')) {
167      namespaces.push('typescript-eslint:*');
168    }
169    if (
170      extra.debugLevel.has('eslint') ||
171      // make sure we don't turn off the eslint debug if it was enabled via --debug
172      debug.enabled('eslint:*,-eslint:code-path')
173    ) {
174      // https://github.com/eslint/eslint/blob/9dfc8501fb1956c90dc11e6377b4cb38a6bea65d/bin/eslint.js#L25
175      namespaces.push('eslint:*,-eslint:code-path');
176    }
177    debug.enable(namespaces.join(','));
178  }
179
180  /**
181   * Track range information in the AST
182   */
183  extra.range = typeof options.range === 'boolean' && options.range;
184  extra.loc = typeof options.loc === 'boolean' && options.loc;
185
186  /**
187   * Track tokens in the AST
188   */
189  if (typeof options.tokens === 'boolean' && options.tokens) {
190    extra.tokens = [];
191  }
192
193  /**
194   * Track comments in the AST
195   */
196  if (typeof options.comment === 'boolean' && options.comment) {
197    extra.comment = true;
198    extra.comments = [];
199  }
200
201  /**
202   * Enable JSX - note the applicable file extension is still required
203   */
204  if (typeof options.jsx === 'boolean' && options.jsx) {
205    extra.jsx = true;
206  }
207
208  /**
209   * Get the file path
210   */
211  if (typeof options.filePath === 'string' && options.filePath !== '<input>') {
212    extra.filePath = options.filePath;
213  } else {
214    extra.filePath = getFileName(extra);
215  }
216
217  /**
218   * The JSX AST changed the node type for string literals
219   * inside a JSX Element from `Literal` to `JSXText`.
220   *
221   * When value is `true`, these nodes will be parsed as type `JSXText`.
222   * When value is `false`, these nodes will be parsed as type `Literal`.
223   */
224  if (typeof options.useJSXTextNode === 'boolean' && options.useJSXTextNode) {
225    extra.useJSXTextNode = true;
226  }
227
228  /**
229   * Allow the user to cause the parser to error if it encounters an unknown AST Node Type
230   * (used in testing)
231   */
232  if (
233    typeof options.errorOnUnknownASTType === 'boolean' &&
234    options.errorOnUnknownASTType
235  ) {
236    extra.errorOnUnknownASTType = true;
237  }
238
239  /**
240   * Allow the user to override the function used for logging
241   */
242  if (typeof options.loggerFn === 'function') {
243    extra.log = options.loggerFn;
244  } else if (options.loggerFn === false) {
245    extra.log = (): void => {};
246  }
247
248  if (typeof options.tsconfigRootDir === 'string') {
249    extra.tsconfigRootDir = options.tsconfigRootDir;
250  }
251
252  // NOTE - ensureAbsolutePath relies upon having the correct tsconfigRootDir in extra
253  extra.filePath = ensureAbsolutePath(extra.filePath, extra);
254
255  // NOTE - prepareAndTransformProjects relies upon having the correct tsconfigRootDir in extra
256  const projectFolderIgnoreList = (options.projectFolderIgnoreList ?? [])
257    .reduce<string[]>((acc, folder) => {
258      if (typeof folder === 'string') {
259        acc.push(folder);
260      }
261      return acc;
262    }, [])
263    // prefix with a ! for not match glob
264    .map(folder => (folder.startsWith('!') ? folder : `!${folder}`));
265  extra.projects = prepareAndTransformProjects(
266    options.project,
267    projectFolderIgnoreList,
268  );
269
270  if (
271    Array.isArray(options.extraFileExtensions) &&
272    options.extraFileExtensions.every(ext => typeof ext === 'string')
273  ) {
274    extra.extraFileExtensions = options.extraFileExtensions;
275  }
276
277  /**
278   * Allow the user to enable or disable the preservation of the AST node maps
279   * during the conversion process.
280   */
281  if (typeof options.preserveNodeMaps === 'boolean') {
282    extra.preserveNodeMaps = options.preserveNodeMaps;
283  }
284
285  extra.createDefaultProgram =
286    typeof options.createDefaultProgram === 'boolean' &&
287    options.createDefaultProgram;
288
289  extra.EXPERIMENTAL_useSourceOfProjectReferenceRedirect =
290    typeof options.EXPERIMENTAL_useSourceOfProjectReferenceRedirect ===
291      'boolean' && options.EXPERIMENTAL_useSourceOfProjectReferenceRedirect;
292}
293
294function warnAboutTSVersion(): void {
295  if (!isRunningSupportedTypeScriptVersion && !warnedAboutTSVersion) {
296    const isTTY = typeof process === undefined ? false : process.stdout?.isTTY;
297    if (isTTY) {
298      const border = '=============';
299      const versionWarning = [
300        border,
301        'WARNING: You are currently running a version of TypeScript which is not officially supported by @typescript-eslint/typescript-estree.',
302        'You may find that it works just fine, or you may not.',
303        `SUPPORTED TYPESCRIPT VERSIONS: ${SUPPORTED_TYPESCRIPT_VERSIONS}`,
304        `YOUR TYPESCRIPT VERSION: ${ACTIVE_TYPESCRIPT_VERSION}`,
305        'Please only submit bug reports when using the officially supported version.',
306        border,
307      ];
308      extra.log(versionWarning.join('\n\n'));
309    }
310    warnedAboutTSVersion = true;
311  }
312}
313
314// eslint-disable-next-line @typescript-eslint/no-empty-interface
315interface EmptyObject {}
316type AST<T extends TSESTreeOptions> = TSESTree.Program &
317  (T['tokens'] extends true ? { tokens: TSESTree.Token[] } : EmptyObject) &
318  (T['comment'] extends true ? { comments: TSESTree.Comment[] } : EmptyObject);
319
320interface ParseAndGenerateServicesResult<T extends TSESTreeOptions> {
321  ast: AST<T>;
322  services: ParserServices;
323}
324interface ParseWithNodeMapsResult<T extends TSESTreeOptions> {
325  ast: AST<T>;
326  esTreeNodeToTSNodeMap: ParserServices['esTreeNodeToTSNodeMap'];
327  tsNodeToESTreeNodeMap: ParserServices['tsNodeToESTreeNodeMap'];
328}
329
330function parse<T extends TSESTreeOptions = TSESTreeOptions>(
331  code: string,
332  options?: T,
333): AST<T> {
334  const { ast } = parseWithNodeMaps(code, options);
335  return ast;
336}
337
338function parseWithNodeMaps<T extends TSESTreeOptions = TSESTreeOptions>(
339  code: string,
340  options?: T,
341): ParseWithNodeMapsResult<T> {
342  /**
343   * Reset the parse configuration
344   */
345  resetExtra();
346
347  /**
348   * Ensure users do not attempt to use parse() when they need parseAndGenerateServices()
349   */
350  if (options?.errorOnTypeScriptSyntacticAndSemanticIssues) {
351    throw new Error(
352      `"errorOnTypeScriptSyntacticAndSemanticIssues" is only supported for parseAndGenerateServices()`,
353    );
354  }
355
356  /**
357   * Ensure the source code is a string, and store a reference to it
358   */
359  code = enforceString(code);
360  extra.code = code;
361
362  /**
363   * Apply the given parser options
364   */
365  if (typeof options !== 'undefined') {
366    applyParserOptionsToExtra(options);
367  }
368
369  /**
370   * Warn if the user is using an unsupported version of TypeScript
371   */
372  warnAboutTSVersion();
373
374  /**
375   * Create a ts.SourceFile directly, no ts.Program is needed for a simple
376   * parse
377   */
378  const ast = createSourceFile(code, extra);
379
380  /**
381   * Convert the TypeScript AST to an ESTree-compatible one
382   */
383  const { estree, astMaps } = astConverter(ast, extra, false);
384
385  return {
386    ast: estree as AST<T>,
387    esTreeNodeToTSNodeMap: astMaps.esTreeNodeToTSNodeMap,
388    tsNodeToESTreeNodeMap: astMaps.tsNodeToESTreeNodeMap,
389  };
390}
391
392function parseAndGenerateServices<T extends TSESTreeOptions = TSESTreeOptions>(
393  code: string,
394  options: T,
395): ParseAndGenerateServicesResult<T> {
396  /**
397   * Reset the parse configuration
398   */
399  resetExtra();
400
401  /**
402   * Ensure the source code is a string, and store a reference to it
403   */
404  code = enforceString(code);
405  extra.code = code;
406
407  /**
408   * Apply the given parser options
409   */
410  if (typeof options !== 'undefined') {
411    applyParserOptionsToExtra(options);
412    if (
413      typeof options.errorOnTypeScriptSyntacticAndSemanticIssues ===
414        'boolean' &&
415      options.errorOnTypeScriptSyntacticAndSemanticIssues
416    ) {
417      extra.errorOnTypeScriptSyntacticAndSemanticIssues = true;
418    }
419  }
420
421  /**
422   * Warn if the user is using an unsupported version of TypeScript
423   */
424  warnAboutTSVersion();
425
426  /**
427   * Generate a full ts.Program in order to be able to provide parser
428   * services, such as type-checking
429   */
430  const shouldProvideParserServices =
431    extra.projects && extra.projects.length > 0;
432  const { ast, program } = getProgramAndAST(
433    code,
434    shouldProvideParserServices,
435    extra.createDefaultProgram,
436  )!;
437
438  /**
439   * Convert the TypeScript AST to an ESTree-compatible one, and optionally preserve
440   * mappings between converted and original AST nodes
441   */
442  const preserveNodeMaps =
443    typeof extra.preserveNodeMaps === 'boolean' ? extra.preserveNodeMaps : true;
444  const { estree, astMaps } = astConverter(ast, extra, preserveNodeMaps);
445
446  /**
447   * Even if TypeScript parsed the source code ok, and we had no problems converting the AST,
448   * there may be other syntactic or semantic issues in the code that we can optionally report on.
449   */
450  if (program && extra.errorOnTypeScriptSyntacticAndSemanticIssues) {
451    const error = getFirstSemanticOrSyntacticError(program, ast);
452    if (error) {
453      throw convertError(error);
454    }
455  }
456
457  /**
458   * Return the converted AST and additional parser services
459   */
460  return {
461    ast: estree as AST<T>,
462    services: {
463      hasFullTypeInformation: shouldProvideParserServices,
464      program,
465      esTreeNodeToTSNodeMap: astMaps.esTreeNodeToTSNodeMap,
466      tsNodeToESTreeNodeMap: astMaps.tsNodeToESTreeNodeMap,
467    },
468  };
469}
470
471export {
472  AST,
473  parse,
474  parseAndGenerateServices,
475  parseWithNodeMaps,
476  ParseAndGenerateServicesResult,
477  ParseWithNodeMapsResult,
478};
479