@linaria/utils
Version:
Blazing fast zero-runtime CSS in JS library
547 lines (541 loc) • 19.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.applyAction = applyAction;
exports.dereference = dereference;
exports.findActionForNode = findActionForNode;
exports.mutate = mutate;
exports.reference = reference;
exports.referenceAll = referenceAll;
exports.removeWithRelated = removeWithRelated;
var _types = require("@babel/types");
var _findIdentifiers = _interopRequireWildcard(require("./findIdentifiers"));
var _getScope = require("./getScope");
var _isNotNull = _interopRequireDefault(require("./isNotNull"));
var _isRemoved = _interopRequireDefault(require("./isRemoved"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
/* eslint-disable no-restricted-syntax */
/* eslint @typescript-eslint/no-use-before-define: ["error", { "functions": false }] */
function validateField(node, key, val, field) {
if (!(field != null && field.validate)) return true;
if (field.optional && val == null) return true;
try {
field.validate(node, key, val);
return true;
} catch {
return false;
}
}
function getBinding(path) {
const binding = (0, _getScope.getScope)(path).getBinding(path.node.name);
if (!binding) {
return undefined;
}
return binding;
}
function reference(path, referencePath = path, force = false) {
if (!force && !path.isReferencedIdentifier()) return;
const binding = getBinding(path);
if (!binding) return;
if (binding.referencePaths.includes(referencePath)) {
return;
}
binding.referenced = true;
binding.referencePaths.push(referencePath !== null && referencePath !== void 0 ? referencePath : path);
binding.references = binding.referencePaths.length;
}
function isReferenced(binding) {
const {
kind,
referenced,
referencePaths,
path
} = binding;
if (path.isFunctionExpression() && path.key === 'init' && path.parentPath.isVariableDeclarator()) {
// It is a function expression in a variable declarator
const id = path.parentPath.get('id');
if (id.isIdentifier()) {
const idBinding = getBinding(id);
return idBinding ? isReferenced(idBinding) : true;
}
return true;
}
if (!referenced) {
return false;
}
// If it's a param binding, we can't just remove it
// because it brakes the function signature. Keep it alive for now.
if (kind === 'param') {
return true;
}
// If all remaining references are in TS/Flow types, binding is unreferenced
return referencePaths.length > 0 || referencePaths.every(i => i.find(ancestor => ancestor.isTSType() || ancestor.isFlowType()));
}
function isReferencedConstantViolation(path, binding) {
if (path.find(p => p === binding.path)) {
// function a(flag) { return (a = function(flag) { flag ? 1 : 2 }) }
// ^ Looks crazy, yeh? Welcome to the wonderful world of transpilers!
// `a = …` here isn't a reference.
return false;
}
if (!path.isReferenced()) {
return false;
}
if (path.isAssignmentExpression() && path.parentPath.isExpressionStatement()) {
// A root assignment without a parent expression statement is not a reference
return false;
}
return true;
}
function dereference(path) {
const binding = getBinding(path);
if (!binding) return null;
const isReference = binding.referencePaths.includes(path);
let referencesInConstantViolations = binding.constantViolations.filter(i => isReferencedConstantViolation(i, binding));
const isConstantViolation = referencesInConstantViolations.includes(path);
if (!isReference && !isConstantViolation) {
return null;
}
if (isReference) {
binding.referencePaths = binding.referencePaths.filter(i => i !== path);
binding.references -= 1;
} else {
referencesInConstantViolations = referencesInConstantViolations.filter(i => i !== path);
}
const nonTypeReferences = binding.referencePaths.filter(_findIdentifiers.nonType);
binding.referenced = nonTypeReferences.length + referencesInConstantViolations.length > 0;
return binding;
}
function dereferenceAll(path) {
return (0, _findIdentifiers.default)([path]).map(identifierPath => dereference(identifierPath)).filter(_isNotNull.default);
}
function referenceAll(path) {
(0, _findIdentifiers.default)([path]).forEach(identifierPath => reference(identifierPath));
}
const deletingNodes = new WeakSet();
const isEmptyList = list => list.length === 0 || list.every(i => deletingNodes.has(i));
const getPathFromAction = action => {
if (!Array.isArray(action)) {
return action;
}
if (action[0] === 'replace' || action[0] === 'remove') {
return action[1];
}
throw new Error(`Unknown action type: ${action[0]}`);
};
function isPrototypeAssignment(path) {
if (!path.isAssignmentExpression()) {
return false;
}
const {
left
} = path.node;
if (!left) {
return false;
}
if (left.type !== 'MemberExpression') {
return false;
}
const {
object,
property
} = left;
if (!object || !property) {
return false;
}
return object.type === 'MemberExpression' && object.property.type === 'Identifier' && object.property.name === 'prototype';
}
function canFunctionBeDelete(fnPath) {
if (isPrototypeAssignment(fnPath.parentPath)) {
// It is a prototype assignment, we can't delete it since we can't find all usages
return false;
}
const fnScope = fnPath.scope;
const parentScope = fnScope.parent;
if (parentScope.parent) {
// It isn't a top-level function, so we can't delete it
return true;
}
if (fnPath.listKey === 'arguments') {
// It is passed as an argument to another function, we can't delete it
return true;
}
return false;
}
function findActionForNode(path) {
if ((0, _isRemoved.default)(path)) return null;
deletingNodes.add(path);
const parent = path.parentPath;
if (!parent) return ['remove', path];
if (parent.isProgram()) {
// Do not delete Program node
return ['remove', path];
}
if (parent.isClassDeclaration() || parent.isClassExpression()) {
if (path.key === 'body') {
return ['replace', path, {
type: 'ClassBody',
body: []
}];
}
}
if (parent.isFunction()) {
if (path.listKey === 'params') {
// Do not remove params of functions
return null;
}
if (path.isBlockStatement() && isEmptyList(path.get('body')) || path === parent.get('body')) {
if (!canFunctionBeDelete(parent)) {
return ['replace', parent, {
...parent.node,
async: false,
body: {
type: 'BlockStatement',
body: [],
directives: []
},
generator: false,
params: []
}];
}
}
}
if (parent.isConditionalExpression()) {
if (path.key === 'test') {
return ['replace', parent, parent.node.alternate];
}
if (path.key === 'consequent') {
return ['replace', path, {
type: 'Identifier',
name: 'undefined'
}];
}
if (path.key === 'alternate') {
return ['replace', path, {
type: 'Identifier',
name: 'undefined'
}];
}
}
if (parent.isLogicalExpression({
operator: '&&'
})) {
return ['replace', parent, {
type: 'BooleanLiteral',
value: false
}];
}
if (parent.isLogicalExpression({
operator: '||'
})) {
return ['replace', parent, path.key === 'left' ? parent.node.right : parent.node.left];
}
if (parent.isObjectProperty()) {
// let's check if it is a special case with Object.defineProperty
const key = parent.get('key');
if (key.isIdentifier({
name: 'get'
})) {
const maybeDefineProperty = parent.parentPath.parentPath;
if (maybeDefineProperty !== null && maybeDefineProperty !== void 0 && maybeDefineProperty.isCallExpression() && maybeDefineProperty.get('callee').matchesPattern('Object.defineProperty')) {
return findActionForNode(maybeDefineProperty);
}
}
return findActionForNode(parent);
}
if (parent.isTemplateLiteral()) {
return ['replace', path, {
type: 'StringLiteral',
value: ''
}];
}
if (parent.isAssignmentExpression()) {
return findActionForNode(parent);
}
if (parent.isCallExpression()) {
return findActionForNode(parent);
}
if (parent.isForInStatement({
left: path.node
})) {
return findActionForNode(parent);
}
if (parent.isFunctionExpression({
body: path.node
}) || parent.isFunctionDeclaration() || parent.isObjectMethod() || parent.isClassMethod()) {
return findActionForNode(parent);
}
if (parent.isBlockStatement()) {
const body = parent.get('body');
if (isEmptyList(body)) {
return findActionForNode(parent);
}
if (path.listKey === 'body' && typeof path.key === 'number') {
if (path.key > 0) {
// We can check whether the previous one can be removed
const prevStatement = body[path.key - 1];
if (prevStatement.isIfStatement() && prevStatement.get('consequent').isReturnStatement()) {
// It's `if (…) return …`, we can remove it.
return findActionForNode(prevStatement);
}
} else if (body.slice(1).every(statement => deletingNodes.has(statement))) {
// If it is the first statement and all other statements
// are marked for deletion, we can remove the whole block.
return findActionForNode(parent);
}
}
}
if (parent.isVariableDeclarator()) {
return findActionForNode(parent);
}
if (parent.isExportNamedDeclaration() && (path.key === 'specifiers' && isEmptyList(parent.get('specifiers')) || path.key === 'declaration' && parent.node.declaration === path.node)) {
return findActionForNode(parent);
}
for (const key of ['body', 'declarations', 'specifiers']) {
if (path.listKey === key && typeof path.key === 'number') {
const list = parent.get(key);
if (isEmptyList(list)) {
return findActionForNode(parent);
}
}
}
if (parent.isTryStatement()) {
return findActionForNode(parent);
}
if (!path.listKey && path.key) {
const field = _types.NODE_FIELDS[parent.type][path.key];
if (!validateField(parent.node, path.key, null, field)) {
// The parent node isn't valid without this field, so we should remove it also.
return findActionForNode(parent);
}
}
for (const key of ['argument', 'block', 'body', 'callee', 'discriminant', 'expression', 'id', 'left', 'object', 'property', 'right', 'test']) {
if (path.key === key && parent.get(key) === path) {
return findActionForNode(parent);
}
}
return ['remove', path];
}
// @babel/preset-typescript transpiles enums, but doesn't reference used identifiers.
function referenceEnums(program) {
/*
* We are looking for transpiled enums.
* (function (Colors) {
* Colors["BLUE"] = "#27509A";
* })(Colors || (Colors = {}));
*/
program.traverse({
ExpressionStatement(expressionStatement) {
const expression = expressionStatement.get('expression');
if (!expression.isCallExpression()) return;
const callee = expression.get('callee');
const args = expression.get('arguments');
if (!callee.isFunctionExpression() || args.length !== 1) return;
const [arg] = args;
if (arg.isLogicalExpression({
operator: '||'
})) {
referenceAll(arg);
}
}
});
}
const fixed = new WeakSet();
function removeUnreferenced(items) {
const referenced = new Set();
items.forEach(item => {
if (!item.node || (0, _isRemoved.default)(item)) return;
const binding = (0, _getScope.getScope)(item).getBinding(item.node.name);
if (!binding) return;
const hasReferences = binding.referencePaths.filter(i => !(0, _isRemoved.default)(i)).length > 0;
if (hasReferences) {
referenced.add(item);
return;
}
const forDeleting = [binding.path, ...binding.constantViolations].map(findActionForNode).filter(_isNotNull.default).map(getPathFromAction);
if (forDeleting.length === 0) return;
(0, _findIdentifiers.default)(forDeleting).forEach(identifier => {
referenced.add(identifier);
});
removeWithRelated(forDeleting);
});
const result = [...referenced];
result.sort((a, b) => {
var _a$node, _b$node;
return (_a$node = a.node) === null || _a$node === void 0 ? void 0 : _a$node.name.localeCompare((_b$node = b.node) === null || _b$node === void 0 ? void 0 : _b$node.name);
});
return result;
}
function getNodeForValue(value) {
if (typeof value === 'string') {
return {
type: 'StringLiteral',
value
};
}
if (typeof value === 'number') {
return {
type: 'NumericLiteral',
value
};
}
if (typeof value === 'boolean') {
return {
type: 'BooleanLiteral',
value
};
}
if (value === null) {
return {
type: 'NullLiteral'
};
}
if (value === undefined) {
return {
type: 'Identifier',
name: 'undefined'
};
}
return undefined;
}
function staticEvaluate(path) {
if (!path) return;
const evaluated = path.evaluate();
if (evaluated.confident) {
const node = getNodeForValue(evaluated.value);
if (node) {
applyAction(['replace', path, node]);
return;
}
}
if (path.isIfStatement()) {
const test = path.get('test');
if (!test.isBooleanLiteral()) {
return;
}
const {
consequent,
alternate
} = path.node;
if (test.node.value) {
applyAction(['replace', path, consequent]);
} else if (alternate) {
applyAction(['replace', path, alternate]);
} else {
applyAction(['remove', path]);
}
}
}
function applyAction(action) {
mutate(action[1], p => {
if ((0, _isRemoved.default)(p)) return;
const parent = p.parentPath;
if (action[0] === 'remove') {
p.remove();
}
if (action[0] === 'replace') {
p.replaceWith(action[2]);
}
staticEvaluate(parent);
});
}
function removeWithRelated(paths) {
if (paths.length === 0) return;
const rootPath = (0, _getScope.getScope)(paths[0]).getProgramParent().path;
if (!fixed.has(rootPath)) {
// Some libraries don't care about bindings, references, and other staff
// So we have to fix the scope before we can detect unused code
referenceEnums(rootPath);
fixed.add(rootPath);
}
const actions = paths.map(findActionForNode).filter(_isNotNull.default);
const affectedPaths = actions.map(getPathFromAction);
let referencedIdentifiers = (0, _findIdentifiers.default)(affectedPaths, 'reference');
referencedIdentifiers.sort((a, b) => {
var _a$node2, _b$node2;
return (_a$node2 = a.node) === null || _a$node2 === void 0 ? void 0 : _a$node2.name.localeCompare((_b$node2 = b.node) === null || _b$node2 === void 0 ? void 0 : _b$node2.name);
});
const referencesOfBinding = (0, _findIdentifiers.default)(affectedPaths, 'declaration').map(i => {
var _ref;
return (_ref = i.node && (0, _getScope.getScope)(i).getBinding(i.node.name)) !== null && _ref !== void 0 ? _ref : null;
}).filter(_isNotNull.default).reduce((acc, i) => [...acc, ...i.referencePaths.filter(_findIdentifiers.nonType)], []).filter(ref =>
// Do not remove `export default function`
!ref.isExportDefaultDeclaration() || !ref.get('declaration').isFunctionDeclaration());
actions.forEach(applyAction);
removeWithRelated(referencesOfBinding);
let clean = false;
while (!clean && referencedIdentifiers.length > 0) {
const referenced = removeUnreferenced(referencedIdentifiers);
clean = referenced.map(i => {
var _i$node;
return (_i$node = i.node) === null || _i$node === void 0 ? void 0 : _i$node.name;
}).join('|') === referencedIdentifiers.map(i => {
var _i$node2;
return (_i$node2 = i.node) === null || _i$node2 === void 0 ? void 0 : _i$node2.name;
}).join('|');
referencedIdentifiers = referenced;
}
}
function mutate(path, fn) {
const dereferenced = dereferenceAll(path);
const mutated = fn(path);
referenceAll(path);
mutated === null || mutated === void 0 || mutated.forEach(p => referenceAll(p));
const dead = dereferenced.filter(p => !isReferenced(p));
const forDeleting = [];
dead.forEach(binding => {
const assignments = [binding.path, ...binding.constantViolations];
assignments.forEach(assignment => {
const {
scope
} = assignment;
const declared = Object.values(assignment.getOuterBindingIdentifiers(false));
if (declared.length === 1 && 'name' in declared[0] && declared[0].name === binding.identifier.name) {
const init = assignment.get('init');
if (!Array.isArray(init) && init !== null && init !== void 0 && init.isAssignmentExpression()) {
var _assignment$parentPat;
// `const a = b = 1` → `b = 1`
(_assignment$parentPat = assignment.parentPath) === null || _assignment$parentPat === void 0 || _assignment$parentPat.replaceWith({
type: 'ExpressionStatement',
expression: init.node
});
const left = init.get('left');
if (left.isIdentifier()) {
// If it was forcefully referenced in the shaker
dereference(left);
}
return;
}
// Only one identifier is declared, so we can remove the whole declaration
forDeleting.push(assignment);
return;
}
if (declared.every(identifier => {
var _scope$getBinding;
return identifier.type === 'Identifier' && !((_scope$getBinding = scope.getBinding(identifier.name)) !== null && _scope$getBinding !== void 0 && _scope$getBinding.referenced);
})) {
// No other identifier is referenced, so we can remove the whole declaration
forDeleting.push(assignment);
return;
}
// We can't remove the binding, but we can remove the part of it
assignment.traverse({
Identifier(identifier) {
if (identifier.node.name === binding.identifier.name) {
const parent = identifier.parentPath;
if (parent.isArrayPattern() && identifier.listKey === 'elements' && typeof identifier.key === 'number') {
parent.node.elements[identifier.key] = null;
} else if (parent.isObjectProperty()) {
forDeleting.push(parent);
}
}
}
});
});
});
removeWithRelated(forDeleting);
}
//# sourceMappingURL=scopeHelpers.js.map