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