-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathparse.js
174 lines (150 loc) · 6.84 KB
/
parse.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
'use strict';
exports.__esModule = true;
/** @typedef {`.${string}`} Extension */
/** @typedef {NonNullable<import('eslint').Rule.RuleContext['settings']> & { 'import/extensions'?: Extension[], 'import/parsers'?: { [k: string]: Extension[] }, 'import/cache'?: { lifetime: number | '∞' | 'Infinity' } }} ESLintSettings */
const moduleRequire = require('./module-require').default;
const extname = require('path').extname;
const fs = require('fs');
const log = require('debug')('eslint-plugin-import:parse');
/** @type {(parserPath: NonNullable<import('eslint').Rule.RuleContext['parserPath']>) => unknown} */
function getBabelEslintVisitorKeys(parserPath) {
if (parserPath.endsWith('index.js')) {
const hypotheticalLocation = parserPath.replace('index.js', 'visitor-keys.js');
if (fs.existsSync(hypotheticalLocation)) {
const keys = moduleRequire(hypotheticalLocation);
return keys.default || keys;
}
}
return null;
}
/** @type {(parserPath: import('eslint').Rule.RuleContext['parserPath'], parserInstance: { VisitorKeys: unknown }, parsedResult?: { visitorKeys?: unknown }) => unknown} */
function keysFromParser(parserPath, parserInstance, parsedResult) {
// Exposed by @typescript-eslint/parser and @babel/eslint-parser
if (parsedResult && parsedResult.visitorKeys) {
return parsedResult.visitorKeys;
}
if (typeof parserPath === 'string' && (/.*espree.*/).test(parserPath)) {
return parserInstance.VisitorKeys;
}
if (typeof parserPath === 'string' && (/.*babel-eslint.*/).test(parserPath)) {
return getBabelEslintVisitorKeys(parserPath);
}
return null;
}
// this exists to smooth over the unintentional breaking change in v2.7.
// TODO, semver-major: avoid mutating `ast` and return a plain object instead.
/** @type {<T extends import('eslint').AST.Program>(ast: T, visitorKeys: unknown) => T} */
function makeParseReturn(ast, visitorKeys) {
if (ast) {
// @ts-expect-error see TODO
ast.visitorKeys = visitorKeys;
// @ts-expect-error see TODO
ast.ast = ast;
}
return ast;
}
/** @type {(text: string) => string} */
function stripUnicodeBOM(text) {
return text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
}
/** @type {(text: string) => string} */
function transformHashbang(text) {
return text.replace(/^#!([^\r\n]+)/u, (_, captured) => `//${captured}`);
}
/** @type {import('./parse').default} */
exports.default = function parse(path, content, context) {
if (context == null) { throw new Error('need context to parse properly'); }
// ESLint in "flat" mode only sets context.languageOptions.parserOptions
let parserOptions = context.languageOptions && context.languageOptions.parserOptions || context.parserOptions;
const parserOrPath = getParser(path, context);
if (!parserOrPath) { throw new Error('parserPath or languageOptions.parser is required!'); }
// hack: espree blows up with frozen options
parserOptions = Object.assign({}, parserOptions);
parserOptions.ecmaFeatures = Object.assign({}, parserOptions.ecmaFeatures);
// always include comments and tokens (for doc parsing)
parserOptions.comment = true;
parserOptions.attachComment = true; // keeping this for backward-compat with older parsers
parserOptions.tokens = true;
// attach node locations
parserOptions.loc = true;
parserOptions.range = true;
// provide the `filePath` like eslint itself does, in `parserOptions`
// https://github.com/eslint/eslint/blob/3ec436ee/lib/linter.js#L637
parserOptions.filePath = path;
// @typescript-eslint/parser will parse the entire project with typechecking if you provide
// "project" or "projects" in parserOptions. Removing these options means the parser will
// only parse one file in isolate mode, which is much, much faster.
// https://github.com/import-js/eslint-plugin-import/issues/1408#issuecomment-509298962
delete parserOptions.EXPERIMENTAL_useProjectService;
delete parserOptions.project;
delete parserOptions.projects;
// require the parser relative to the main module (i.e., ESLint)
const parser = typeof parserOrPath === 'string' ? moduleRequire(parserOrPath) : parserOrPath;
// replicate bom strip and hashbang transform of ESLint
// https://github.com/eslint/eslint/blob/b93af98b3c417225a027cabc964c38e779adb945/lib/linter/linter.js#L779
content = transformHashbang(stripUnicodeBOM(String(content)));
if (typeof parser.parseForESLint === 'function') {
let ast;
try {
const parserRaw = parser.parseForESLint(content, parserOptions);
ast = parserRaw.ast;
// @ts-expect-error TODO: FIXME
return makeParseReturn(ast, keysFromParser(parserOrPath, parser, parserRaw));
} catch (e) {
console.warn();
console.warn('Error while parsing ' + parserOptions.filePath);
// @ts-expect-error e is almost certainly an Error here
console.warn('Line ' + e.lineNumber + ', column ' + e.column + ': ' + e.message);
}
if (!ast || typeof ast !== 'object') {
console.warn(
// Can only be invalid for custom parser per imports/parser
'`parseForESLint` from parser `' + (typeof parserOrPath === 'string' ? parserOrPath : '`context.languageOptions.parser`') + '` is invalid and will just be ignored'
);
} else {
// @ts-expect-error TODO: FIXME
return makeParseReturn(ast, keysFromParser(parserOrPath, parser, undefined));
}
}
const ast = parser.parse(content, parserOptions);
// @ts-expect-error TODO: FIXME
return makeParseReturn(ast, keysFromParser(parserOrPath, parser, undefined));
};
/** @type {(path: string, context: import('eslint').Rule.RuleContext) => string | null | (import('eslint').Linter.ParserModule)} */
function getParser(path, context) {
const parserPath = getParserPath(path, context);
if (parserPath) {
return parserPath;
}
if (
!!context.languageOptions
&& !!context.languageOptions.parser
&& typeof context.languageOptions.parser !== 'string'
&& (
// @ts-expect-error TODO: figure out a better type
typeof context.languageOptions.parser.parse === 'function'
// @ts-expect-error TODO: figure out a better type
|| typeof context.languageOptions.parser.parseForESLint === 'function'
)
) {
return context.languageOptions.parser;
}
return null;
}
/** @type {(path: string, context: import('eslint').Rule.RuleContext & { settings?: ESLintSettings }) => import('eslint').Rule.RuleContext['parserPath']} */
function getParserPath(path, context) {
const parsers = context.settings['import/parsers'];
if (parsers != null) {
// eslint-disable-next-line no-extra-parens
const extension = /** @type {Extension} */ (extname(path));
for (const parserPath in parsers) {
if (parsers[parserPath].indexOf(extension) > -1) {
// use this alternate parser
log('using alt parser:', parserPath);
return parserPath;
}
}
}
// default to use ESLint parser
return context.parserPath;
}