fixclosure
Version:
JavaScript dependency checker/fixer for Closure Library based on ECMAScript AST
517 lines (516 loc) • 18.5 kB
JavaScript
;
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;
}