UNPKG

18.5 kBJavaScriptView Raw
1"use strict";
2var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3 if (k2 === undefined) k2 = k;
4 var desc = Object.getOwnPropertyDescriptor(m, k);
5 if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6 desc = { enumerable: true, get: function() { return m[k]; } };
7 }
8 Object.defineProperty(o, k2, desc);
9}) : (function(o, m, k, k2) {
10 if (k2 === undefined) k2 = k;
11 o[k2] = m[k];
12}));
13var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14 Object.defineProperty(o, "default", { enumerable: true, value: v });
15}) : function(o, v) {
16 o["default"] = v;
17});
18var __importStar = (this && this.__importStar) || function (mod) {
19 if (mod && mod.__esModule) return mod;
20 var result = {};
21 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22 __setModuleDefault(result, mod);
23 return result;
24};
25var __importDefault = (this && this.__importDefault) || function (mod) {
26 return (mod && mod.__esModule) ? mod : { "default": mod };
27};
28Object.defineProperty(exports, "__esModule", { value: true });
29exports.Parser = void 0;
30const doctrine_1 = __importDefault(require("@teppeis/doctrine"));
31const espree = __importStar(require("espree"));
32const estraverse_fb_1 = require("estraverse-fb");
33const lodash_difference_1 = __importDefault(require("lodash.difference"));
34const def = __importStar(require("./default"));
35const visitor_1 = require("./visitor");
36const tagsHavingType = new Set([
37 "const",
38 "define",
39 "enum",
40 "extends",
41 "implements",
42 "param",
43 "private",
44 "protected",
45 "public",
46 "return",
47 "this",
48 "type",
49 "typedef",
50]);
51class Parser {
52 constructor(opt_options) {
53 this.minLine_ = Number.MAX_VALUE;
54 this.maxLine_ = 0;
55 const options = (this.options = opt_options || {});
56 if (options.provideRoots) {
57 this.provideRoots_ = new Set(options.provideRoots);
58 }
59 else {
60 this.provideRoots_ = def.getRoots();
61 }
62 this.replaceMap_ = def.getReplaceMap();
63 if (options.replaceMap) {
64 options.replaceMap.forEach((value, key) => {
65 this.replaceMap_.set(key, value);
66 });
67 }
68 this.providedNamespaces_ = new Set();
69 if (options.providedNamespace) {
70 options.providedNamespace.forEach((method) => {
71 this.providedNamespaces_.add(method);
72 });
73 }
74 if (options.ignoreProvides != null) {
75 this.ignoreProvides_ = options.ignoreProvides;
76 }
77 else {
78 this.ignoreProvides_ = false;
79 }
80 this.ignorePackages_ = def.getIgnorePackages();
81 }
82 parse(src) {
83 const options = {
84 loc: true,
85 comment: true,
86 ecmaVersion: 2019,
87 sourceType: "script",
88 ecmaFeatures: {
89 jsx: true,
90 },
91 ...this.options.parserOptions,
92 };
93 const program = espree.parse(src, options);
94 const { comments } = program;
95 /* istanbul ignore if */
96 if (!comments) {
97 throw new Error("Enable `comment` option for espree parser");
98 }
99 return this.parseAst(program, comments);
100 }
101 parseAst(program, comments) {
102 const parsed = this.traverseProgram_(program);
103 const provided = this.extractProvided_(parsed);
104 const required = this.extractRequired_(parsed);
105 const requireTyped = this.extractRequireTyped_(parsed);
106 const forwardDeclared = this.extractForwardDeclared_(parsed);
107 const ignored = this.extractIgnored_(parsed, comments);
108 const toProvide = this.ignoreProvides_
109 ? provided
110 : this.extractToProvide_(parsed, comments);
111 const fromJsDoc = this.extractToRequireTypeFromJsDoc_(comments);
112 const toRequire = this.extractToRequire_(parsed, toProvide, comments, fromJsDoc.toRequire);
113 const toRequireType = (0, lodash_difference_1.default)(fromJsDoc.toRequireType, toProvide, toRequire);
114 return {
115 provided,
116 required,
117 requireTyped,
118 forwardDeclared,
119 toProvide,
120 toRequire,
121 toRequireType,
122 toForwardDeclare: [],
123 ignoredProvide: ignored.provide,
124 ignoredRequire: ignored.require,
125 ignoredRequireType: ignored.requireType,
126 ignoredForwardDeclare: ignored.forwardDeclare,
127 // first goog.provide or goog.require line
128 provideStart: this.minLine_,
129 // last goog.provide or goog.require line
130 provideEnd: this.maxLine_,
131 };
132 }
133 extractToProvide_(parsed, comments) {
134 const suppressComments = this.getSuppressProvideComments_(comments);
135 return parsed
136 .filter((namespace) => this.suppressFilter_(suppressComments, namespace))
137 .map((namespace) => this.toProvideMapper_(comments, namespace))
138 .filter(isDefAndNotNull)
139 .filter((provide) => this.provideRootFilter_(provide))
140 .sort()
141 .reduce(uniq, []);
142 }
143 /**
144 * @return true if the node has JSDoc that includes @typedef and not @private
145 * This method assume the JSDoc is at a line just before the node.
146 * Use ESLint context like `context.getJSDocComment(node)` if possible.
147 */
148 hasTypedefAnnotation_(node, comments) {
149 const { line } = getLoc(node).start;
150 const jsDocComments = comments.filter((comment) => getLoc(comment).end.line === line - 1 &&
151 isBlockComment(comment) &&
152 /^\*/.test(comment.value));
153 if (jsDocComments.length === 0) {
154 return false;
155 }
156 return jsDocComments.every((comment) => {
157 const jsdoc = doctrine_1.default.parse(`/*${comment.value}*/`, { unwrap: true });
158 return (jsdoc.tags.some((tag) => tag.title === "typedef") &&
159 !jsdoc.tags.some((tag) => tag.title === "private"));
160 });
161 }
162 getSuppressProvideComments_(comments) {
163 return comments.filter((comment) => isLineComment(comment) &&
164 /^\s*fixclosure\s*:\s*suppressProvide\b/.test(comment.value));
165 }
166 getSuppressRequireComments_(comments) {
167 return comments.filter((comment) => isLineComment(comment) &&
168 /^\s*fixclosure\s*:\s*suppressRequire\b/.test(comment.value));
169 }
170 extractToRequire_(parsed, toProvide, comments, opt_required) {
171 const additional = opt_required || [];
172 const suppressComments = this.getSuppressRequireComments_(comments);
173 const toRequire = parsed
174 .filter((namespace) => this.toRequireFilter_(namespace))
175 .filter((namespace) => this.suppressFilter_(suppressComments, namespace))
176 .map((namespace) => this.toRequireMapper_(namespace))
177 .concat(additional)
178 .filter(isDefAndNotNull)
179 .sort()
180 .reduce(uniq, []);
181 return (0, lodash_difference_1.default)(toRequire, toProvide);
182 }
183 extractToRequireTypeFromJsDoc_(comments) {
184 const toRequire = [];
185 const toRequireType = [];
186 comments
187 .filter((comment) =>
188 // JSDoc Style
189 isBlockComment(comment) && /^\*/.test(comment.value))
190 .forEach((comment) => {
191 const { tags } = doctrine_1.default.parse(`/*${comment.value}*/`, {
192 unwrap: true,
193 });
194 tags
195 .filter((tag) => tagsHavingType.has(tag.title) && tag.type)
196 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
197 .map((tag) => this.extractType(tag.type))
198 .forEach((names) => {
199 toRequireType.push(...names);
200 });
201 tags
202 .filter((tag) => (tag.title === "implements" || tag.title === "extends") &&
203 tag.type)
204 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
205 .map((tag) => this.extractType(tag.type))
206 .forEach((names) => {
207 toRequire.push(...names);
208 });
209 });
210 return {
211 toRequire: toRequire
212 .filter((name) => this.isProvidedNamespace_(name))
213 .sort()
214 .reduce(uniq, []),
215 toRequireType: toRequireType
216 .map((name) => this.getRequiredPackageName_(name))
217 .filter(isDefAndNotNull)
218 .sort()
219 .reduce(uniq, []),
220 };
221 }
222 extractType(type) {
223 if (!type) {
224 return [];
225 }
226 let result;
227 switch (type.type) {
228 case "NameExpression":
229 return [type.name];
230 case "NullableType":
231 case "NonNullableType":
232 case "OptionalType":
233 case "RestType":
234 return this.extractType(type.expression);
235 case "TypeApplication":
236 result = this.extractType(type.expression);
237 result.push(...type.applications.map((app) => this.extractType(app)).flat());
238 break;
239 case "UnionType":
240 return type.elements.map((el) => this.extractType(el)).flat();
241 case "RecordType":
242 return type.fields.map((field) => this.extractType(field)).flat();
243 case "FieldType":
244 if (type.value) {
245 return this.extractType(type.value);
246 }
247 else {
248 return [];
249 }
250 case "FunctionType":
251 result = type.params.map((param) => this.extractType(param)).flat();
252 if (type.result) {
253 result.push(...this.extractType(type.result));
254 }
255 if (type.this) {
256 result.push(...this.extractType(type.this));
257 }
258 break;
259 default:
260 result = [];
261 }
262 return result;
263 }
264 /**
265 * Extract `goog.require('goog.foo') // fixclosure: ignore`.
266 */
267 extractIgnored_(parsed, comments) {
268 const suppresses = comments
269 .filter((comment) => isLineComment(comment) &&
270 /^\s*fixclosure\s*:\s*ignore\b/.test(comment.value))
271 .reduce((prev, item) => {
272 prev[getLoc(item).start.line] = true;
273 return prev;
274 }, {});
275 if (Object.keys(suppresses).length === 0) {
276 return { provide: [], require: [], requireType: [], forwardDeclare: [] };
277 }
278 const getSuppressedNamespaces = (method) => parsed
279 .filter(isSimpleCallExpression)
280 .filter(isCalledMethodName(method))
281 .filter((namespace) => this.updateMinMaxLine_(namespace))
282 .filter((req) => !!suppresses[getLoc(req.node).start.line])
283 .map(getArgStringLiteralOrNull)
284 .filter(isDefAndNotNull)
285 .sort();
286 return {
287 provide: getSuppressedNamespaces("goog.provide"),
288 require: getSuppressedNamespaces("goog.require"),
289 requireType: getSuppressedNamespaces("goog.requireType"),
290 forwardDeclare: getSuppressedNamespaces("goog.forwardDeclare"),
291 };
292 }
293 extractProvided_(parsed) {
294 return this.extractGoogDeclaration_(parsed, "goog.provide");
295 }
296 extractRequired_(parsed) {
297 return this.extractGoogDeclaration_(parsed, "goog.require");
298 }
299 extractRequireTyped_(parsed) {
300 return this.extractGoogDeclaration_(parsed, "goog.requireType");
301 }
302 extractForwardDeclared_(parsed) {
303 return this.extractGoogDeclaration_(parsed, "goog.forwardDeclare");
304 }
305 /**
306 * @param parsed
307 * @param method like 'goog.provide' or 'goog.require'
308 */
309 extractGoogDeclaration_(parsed, method) {
310 return parsed
311 .filter(isSimpleCallExpression)
312 .filter(isCalledMethodName(method))
313 .filter((namespace) => this.updateMinMaxLine_(namespace))
314 .map(getArgStringLiteralOrNull)
315 .filter(isDefAndNotNull)
316 .sort();
317 }
318 traverseProgram_(node) {
319 const uses = [];
320 (0, estraverse_fb_1.traverse)(node, {
321 leave(currentNode, parent) {
322 visitor_1.leave.call(this, currentNode, uses);
323 },
324 });
325 return uses;
326 }
327 /**
328 * @return True if the item has a root namespace to extract.
329 */
330 provideRootFilter_(item) {
331 const root = item.split(".")[0];
332 return this.provideRoots_.has(root);
333 }
334 /**
335 * @return Provided namespace
336 */
337 toProvideMapper_(comments, use) {
338 let name = use.name.join(".");
339 switch (use.node.type) {
340 case "AssignmentExpression":
341 if (use.key === "left" && getLoc(use.node).start.column === 0) {
342 return this.getProvidedPackageName_(name);
343 }
344 break;
345 case "ExpressionStatement":
346 if (this.hasTypedefAnnotation_(use.node, comments)) {
347 const parent = use.name.slice(0, -1);
348 const parentLastname = parent[parent.length - 1];
349 if (/^[A-Z]/.test(parentLastname)) {
350 name = parent.join(".");
351 }
352 return this.getProvidedPackageName_(name);
353 }
354 break;
355 default:
356 break;
357 }
358 return null;
359 }
360 /**
361 * @return Required namespace
362 */
363 toRequireMapper_(use) {
364 const name = use.name.join(".");
365 return this.getRequiredPackageName_(name);
366 }
367 toRequireFilter_(use) {
368 switch (use.node.type) {
369 case "ExpressionStatement":
370 return false;
371 case "AssignmentExpression":
372 if (use.key === "left" && getLoc(use.node).start.column === 0) {
373 return false;
374 }
375 break;
376 default:
377 break;
378 }
379 return true;
380 }
381 /**
382 * Filter toProvide and toRequire if it is suppressed.
383 */
384 suppressFilter_(comments, use) {
385 const start = getLoc(use.node).start.line;
386 const suppressComment = comments.some((comment) => getLoc(comment).start.line + 1 === start);
387 return !suppressComment;
388 }
389 getRequiredPackageName_(name) {
390 let names = name.split(".");
391 do {
392 const name = this.replaceMethod_(names.join("."));
393 if (this.providedNamespaces_.has(name) && !this.isIgnorePackage_(name)) {
394 return name;
395 }
396 names = names.slice(0, -1);
397 } while (names.length > 0);
398 return null;
399 }
400 getProvidedPackageName_(name) {
401 name = this.replaceMethod_(name);
402 let names = name.split(".");
403 let lastname = names[names.length - 1];
404 // Remove prototype or superClass_.
405 names = names.reduceRight((prev, cur) => {
406 if (cur === "prototype") {
407 return [];
408 }
409 else {
410 prev.unshift(cur);
411 return prev;
412 }
413 }, []);
414 if (!this.isProvidedNamespace_(name)) {
415 lastname = names[names.length - 1];
416 if (/^[a-z$]/.test(lastname)) {
417 // Remove the last method name.
418 names.pop();
419 }
420 while (names.length > 0) {
421 lastname = names[names.length - 1];
422 if (/^[A-Z][_0-9A-Z]+$/.test(lastname)) {
423 // Remove the last constant name.
424 names.pop();
425 }
426 else {
427 break;
428 }
429 }
430 }
431 if (this.isPrivateProp_(names)) {
432 return null;
433 }
434 const pkg = names.join(".");
435 if (pkg && !this.isIgnorePackage_(pkg)) {
436 return this.replaceMethod_(pkg);
437 }
438 else {
439 // Ignore just one word namespace like 'goog'.
440 return null;
441 }
442 }
443 isIgnorePackage_(name) {
444 return this.ignorePackages_.has(name);
445 }
446 isPrivateProp_(names) {
447 return names.some((name) => name.endsWith("_"));
448 }
449 replaceMethod_(method) {
450 return this.replaceMap_.has(method)
451 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
452 this.replaceMap_.get(method)
453 : method;
454 }
455 isProvidedNamespace_(name) {
456 return this.providedNamespaces_.has(name);
457 }
458 updateMinMaxLine_(use) {
459 const start = getLoc(use.node).start.line;
460 const end = getLoc(use.node).end.line;
461 this.minLine_ = Math.min(this.minLine_, start);
462 this.maxLine_ = Math.max(this.maxLine_, end);
463 return true;
464 }
465}
466exports.Parser = Parser;
467function isSimpleCallExpression(use) {
468 return use.node.type === "CallExpression";
469}
470function isCalledMethodName(method) {
471 return (use) => use.name.join(".") === method;
472}
473function getArgStringLiteralOrNull(use) {
474 const arg = use.node.arguments[0];
475 if (arg.type === "Literal" && typeof arg.value === "string") {
476 return arg.value;
477 }
478 return null;
479}
480/**
481 * Support both ESTree (Line) and @babel/parser (CommentLine)
482 */
483function isLineComment(comment) {
484 return comment.type === "CommentLine" || comment.type === "Line";
485}
486/**
487 * Support both ESTree (Block) and @babel/parser (CommentBlock)
488 */
489function isBlockComment(comment) {
490 return comment.type === "CommentBlock" || comment.type === "Block";
491}
492/**
493 * Get non-nullable `.loc` (SourceLocation) prop or throw an error
494 */
495function getLoc(node) {
496 /* istanbul ignore if */
497 if (!node.loc) {
498 throw new TypeError(`Enable "loc" option of your parser. The node doesn't have "loc" property: ${node}`);
499 }
500 return node.loc;
501}
502/**
503 * Use like `array.filter(isDefAndNotNull)`
504 */
505function isDefAndNotNull(item) {
506 return item != null;
507}
508/**
509 * Use like `array.reduce(uniq, [])`
510 */
511function uniq(prev, cur) {
512 if (prev[prev.length - 1] !== cur) {
513 prev.push(cur);
514 }
515 return prev;
516}