UNPKG

fixclosure

Version:

JavaScript dependency checker/fixer for Closure Library based on ECMAScript AST

517 lines (516 loc) 18.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Parser = void 0; const doctrine_1 = __importDefault(require("@teppeis/doctrine")); const espree = __importStar(require("espree")); const estraverse_fb_1 = require("estraverse-fb"); const lodash_difference_1 = __importDefault(require("lodash.difference")); const def = __importStar(require("./default")); const visitor_1 = require("./visitor"); const tagsHavingType = new Set([ "const", "define", "enum", "extends", "implements", "param", "private", "protected", "public", "return", "this", "type", "typedef", ]); class Parser { constructor(opt_options) { this.minLine_ = Number.MAX_VALUE; this.maxLine_ = 0; const options = (this.options = opt_options || {}); if (options.provideRoots) { this.provideRoots_ = new Set(options.provideRoots); } else { this.provideRoots_ = def.getRoots(); } this.replaceMap_ = def.getReplaceMap(); if (options.replaceMap) { options.replaceMap.forEach((value, key) => { this.replaceMap_.set(key, value); }); } this.providedNamespaces_ = new Set(); if (options.providedNamespace) { options.providedNamespace.forEach((method) => { this.providedNamespaces_.add(method); }); } if (options.ignoreProvides != null) { this.ignoreProvides_ = options.ignoreProvides; } else { this.ignoreProvides_ = false; } this.ignorePackages_ = def.getIgnorePackages(); } parse(src) { const options = { loc: true, comment: true, ecmaVersion: 2019, sourceType: "script", ecmaFeatures: { jsx: true, }, ...this.options.parserOptions, }; const program = espree.parse(src, options); const { comments } = program; /* istanbul ignore if */ if (!comments) { throw new Error("Enable `comment` option for espree parser"); } return this.parseAst(program, comments); } parseAst(program, comments) { const parsed = this.traverseProgram_(program); const provided = this.extractProvided_(parsed); const required = this.extractRequired_(parsed); const requireTyped = this.extractRequireTyped_(parsed); const forwardDeclared = this.extractForwardDeclared_(parsed); const ignored = this.extractIgnored_(parsed, comments); const toProvide = this.ignoreProvides_ ? provided : this.extractToProvide_(parsed, comments); const fromJsDoc = this.extractToRequireTypeFromJsDoc_(comments); const toRequire = this.extractToRequire_(parsed, toProvide, comments, fromJsDoc.toRequire); const toRequireType = (0, lodash_difference_1.default)(fromJsDoc.toRequireType, toProvide, toRequire); return { provided, required, requireTyped, forwardDeclared, toProvide, toRequire, toRequireType, toForwardDeclare: [], ignoredProvide: ignored.provide, ignoredRequire: ignored.require, ignoredRequireType: ignored.requireType, ignoredForwardDeclare: ignored.forwardDeclare, // first goog.provide or goog.require line provideStart: this.minLine_, // last goog.provide or goog.require line provideEnd: this.maxLine_, }; } extractToProvide_(parsed, comments) { const suppressComments = this.getSuppressProvideComments_(comments); return parsed .filter((namespace) => this.suppressFilter_(suppressComments, namespace)) .map((namespace) => this.toProvideMapper_(comments, namespace)) .filter(isDefAndNotNull) .filter((provide) => this.provideRootFilter_(provide)) .sort() .reduce(uniq, []); } /** * @return true if the node has JSDoc that includes @typedef and not @private * This method assume the JSDoc is at a line just before the node. * Use ESLint context like `context.getJSDocComment(node)` if possible. */ hasTypedefAnnotation_(node, comments) { const { line } = getLoc(node).start; const jsDocComments = comments.filter((comment) => getLoc(comment).end.line === line - 1 && isBlockComment(comment) && /^\*/.test(comment.value)); if (jsDocComments.length === 0) { return false; } return jsDocComments.every((comment) => { const jsdoc = doctrine_1.default.parse(`/*${comment.value}*/`, { unwrap: true }); return (jsdoc.tags.some((tag) => tag.title === "typedef") && !jsdoc.tags.some((tag) => tag.title === "private")); }); } getSuppressProvideComments_(comments) { return comments.filter((comment) => isLineComment(comment) && /^\s*fixclosure\s*:\s*suppressProvide\b/.test(comment.value)); } getSuppressRequireComments_(comments) { return comments.filter((comment) => isLineComment(comment) && /^\s*fixclosure\s*:\s*suppressRequire\b/.test(comment.value)); } extractToRequire_(parsed, toProvide, comments, opt_required) { const additional = opt_required || []; const suppressComments = this.getSuppressRequireComments_(comments); const toRequire = parsed .filter((namespace) => this.toRequireFilter_(namespace)) .filter((namespace) => this.suppressFilter_(suppressComments, namespace)) .map((namespace) => this.toRequireMapper_(namespace)) .concat(additional) .filter(isDefAndNotNull) .sort() .reduce(uniq, []); return (0, lodash_difference_1.default)(toRequire, toProvide); } extractToRequireTypeFromJsDoc_(comments) { const toRequire = []; const toRequireType = []; comments .filter((comment) => // JSDoc Style isBlockComment(comment) && /^\*/.test(comment.value)) .forEach((comment) => { const { tags } = doctrine_1.default.parse(`/*${comment.value}*/`, { unwrap: true, }); tags .filter((tag) => tagsHavingType.has(tag.title) && tag.type) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion .map((tag) => this.extractType(tag.type)) .forEach((names) => { toRequireType.push(...names); }); tags .filter((tag) => (tag.title === "implements" || tag.title === "extends") && tag.type) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion .map((tag) => this.extractType(tag.type)) .forEach((names) => { toRequire.push(...names); }); }); return { toRequire: toRequire .filter((name) => this.isProvidedNamespace_(name)) .sort() .reduce(uniq, []), toRequireType: toRequireType .map((name) => this.getRequiredPackageName_(name)) .filter(isDefAndNotNull) .sort() .reduce(uniq, []), }; } extractType(type) { if (!type) { return []; } let result; switch (type.type) { case "NameExpression": return [type.name]; case "NullableType": case "NonNullableType": case "OptionalType": case "RestType": return this.extractType(type.expression); case "TypeApplication": result = this.extractType(type.expression); result.push(...type.applications.map((app) => this.extractType(app)).flat()); break; case "UnionType": return type.elements.map((el) => this.extractType(el)).flat(); case "RecordType": return type.fields.map((field) => this.extractType(field)).flat(); case "FieldType": if (type.value) { return this.extractType(type.value); } else { return []; } case "FunctionType": result = type.params.map((param) => this.extractType(param)).flat(); if (type.result) { result.push(...this.extractType(type.result)); } if (type.this) { result.push(...this.extractType(type.this)); } break; default: result = []; } return result; } /** * Extract `goog.require('goog.foo') // fixclosure: ignore`. */ extractIgnored_(parsed, comments) { const suppresses = comments .filter((comment) => isLineComment(comment) && /^\s*fixclosure\s*:\s*ignore\b/.test(comment.value)) .reduce((prev, item) => { prev[getLoc(item).start.line] = true; return prev; }, {}); if (Object.keys(suppresses).length === 0) { return { provide: [], require: [], requireType: [], forwardDeclare: [] }; } const getSuppressedNamespaces = (method) => parsed .filter(isSimpleCallExpression) .filter(isCalledMethodName(method)) .filter((namespace) => this.updateMinMaxLine_(namespace)) .filter((req) => !!suppresses[getLoc(req.node).start.line]) .map(getArgStringLiteralOrNull) .filter(isDefAndNotNull) .sort(); return { provide: getSuppressedNamespaces("goog.provide"), require: getSuppressedNamespaces("goog.require"), requireType: getSuppressedNamespaces("goog.requireType"), forwardDeclare: getSuppressedNamespaces("goog.forwardDeclare"), }; } extractProvided_(parsed) { return this.extractGoogDeclaration_(parsed, "goog.provide"); } extractRequired_(parsed) { return this.extractGoogDeclaration_(parsed, "goog.require"); } extractRequireTyped_(parsed) { return this.extractGoogDeclaration_(parsed, "goog.requireType"); } extractForwardDeclared_(parsed) { return this.extractGoogDeclaration_(parsed, "goog.forwardDeclare"); } /** * @param parsed * @param method like 'goog.provide' or 'goog.require' */ extractGoogDeclaration_(parsed, method) { return parsed .filter(isSimpleCallExpression) .filter(isCalledMethodName(method)) .filter((namespace) => this.updateMinMaxLine_(namespace)) .map(getArgStringLiteralOrNull) .filter(isDefAndNotNull) .sort(); } traverseProgram_(node) { const uses = []; (0, estraverse_fb_1.traverse)(node, { leave(currentNode, parent) { visitor_1.leave.call(this, currentNode, uses); }, }); return uses; } /** * @return True if the item has a root namespace to extract. */ provideRootFilter_(item) { const root = item.split(".")[0]; return this.provideRoots_.has(root); } /** * @return Provided namespace */ toProvideMapper_(comments, use) { let name = use.name.join("."); switch (use.node.type) { case "AssignmentExpression": if (use.key === "left" && getLoc(use.node).start.column === 0) { return this.getProvidedPackageName_(name); } break; case "ExpressionStatement": if (this.hasTypedefAnnotation_(use.node, comments)) { const parent = use.name.slice(0, -1); const parentLastname = parent[parent.length - 1]; if (/^[A-Z]/.test(parentLastname)) { name = parent.join("."); } return this.getProvidedPackageName_(name); } break; default: break; } return null; } /** * @return Required namespace */ toRequireMapper_(use) { const name = use.name.join("."); return this.getRequiredPackageName_(name); } toRequireFilter_(use) { switch (use.node.type) { case "ExpressionStatement": return false; case "AssignmentExpression": if (use.key === "left" && getLoc(use.node).start.column === 0) { return false; } break; default: break; } return true; } /** * Filter toProvide and toRequire if it is suppressed. */ suppressFilter_(comments, use) { const start = getLoc(use.node).start.line; const suppressComment = comments.some((comment) => getLoc(comment).start.line + 1 === start); return !suppressComment; } getRequiredPackageName_(name) { let names = name.split("."); do { const name = this.replaceMethod_(names.join(".")); if (this.providedNamespaces_.has(name) && !this.isIgnorePackage_(name)) { return name; } names = names.slice(0, -1); } while (names.length > 0); return null; } getProvidedPackageName_(name) { name = this.replaceMethod_(name); let names = name.split("."); let lastname = names[names.length - 1]; // Remove prototype or superClass_. names = names.reduceRight((prev, cur) => { if (cur === "prototype") { return []; } else { prev.unshift(cur); return prev; } }, []); if (!this.isProvidedNamespace_(name)) { lastname = names[names.length - 1]; if (/^[a-z$]/.test(lastname)) { // Remove the last method name. names.pop(); } while (names.length > 0) { lastname = names[names.length - 1]; if (/^[A-Z][_0-9A-Z]+$/.test(lastname)) { // Remove the last constant name. names.pop(); } else { break; } } } if (this.isPrivateProp_(names)) { return null; } const pkg = names.join("."); if (pkg && !this.isIgnorePackage_(pkg)) { return this.replaceMethod_(pkg); } else { // Ignore just one word namespace like 'goog'. return null; } } isIgnorePackage_(name) { return this.ignorePackages_.has(name); } isPrivateProp_(names) { return names.some((name) => name.endsWith("_")); } replaceMethod_(method) { return this.replaceMap_.has(method) ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.replaceMap_.get(method) : method; } isProvidedNamespace_(name) { return this.providedNamespaces_.has(name); } updateMinMaxLine_(use) { const start = getLoc(use.node).start.line; const end = getLoc(use.node).end.line; this.minLine_ = Math.min(this.minLine_, start); this.maxLine_ = Math.max(this.maxLine_, end); return true; } } exports.Parser = Parser; function isSimpleCallExpression(use) { return use.node.type === "CallExpression"; } function isCalledMethodName(method) { return (use) => use.name.join(".") === method; } function getArgStringLiteralOrNull(use) { const arg = use.node.arguments[0]; if (arg.type === "Literal" && typeof arg.value === "string") { return arg.value; } return null; } /** * Support both ESTree (Line) and @babel/parser (CommentLine) */ function isLineComment(comment) { return comment.type === "CommentLine" || comment.type === "Line"; } /** * Support both ESTree (Block) and @babel/parser (CommentBlock) */ function isBlockComment(comment) { return comment.type === "CommentBlock" || comment.type === "Block"; } /** * Get non-nullable `.loc` (SourceLocation) prop or throw an error */ function getLoc(node) { /* istanbul ignore if */ if (!node.loc) { throw new TypeError(`Enable "loc" option of your parser. The node doesn't have "loc" property: ${node}`); } return node.loc; } /** * Use like `array.filter(isDefAndNotNull)` */ function isDefAndNotNull(item) { return item != null; } /** * Use like `array.reduce(uniq, [])` */ function uniq(prev, cur) { if (prev[prev.length - 1] !== cur) { prev.push(cur); } return prev; }