274 lines
7.8 KiB
JavaScript
274 lines
7.8 KiB
JavaScript
/**
|
|
* @fileoverview Ensures that definitions and uses of properties on the
|
|
* ``lazy`` object are valid.
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
"use strict";
|
|
const helpers = require("../helpers");
|
|
|
|
const items = [
|
|
"loader",
|
|
"XPCOMUtils",
|
|
"Integration",
|
|
"ChromeUtils",
|
|
"DevToolsUtils",
|
|
"Object",
|
|
"Reflect",
|
|
];
|
|
|
|
const callExpressionDefinitions = [
|
|
/^loader\.lazyGetter\(lazy, "(\w+)"/,
|
|
/^loader\.lazyServiceGetter\(lazy, "(\w+)"/,
|
|
/^loader\.lazyRequireGetter\(lazy, "(\w+)"/,
|
|
/^Integration\.downloads\.defineESModuleGetter\(lazy, "(\w+)"/,
|
|
/^ChromeUtils\.defineLazyGetter\(lazy, "(\w+)"/,
|
|
/^XPCOMUtils\.defineLazyPreferenceGetter\(lazy, "(\w+)"/,
|
|
/^XPCOMUtils\.defineLazyScriptGetter\(lazy, "(\w+)"/,
|
|
/^XPCOMUtils\.defineLazyServiceGetter\(lazy, "(\w+)"/,
|
|
/^XPCOMUtils\.defineConstant\(lazy, "(\w+)"/,
|
|
/^DevToolsUtils\.defineLazyGetter\(lazy, "(\w+)"/,
|
|
/^Object\.defineProperty\(lazy, "(\w+)"/,
|
|
/^Reflect\.defineProperty\(lazy, "(\w+)"/,
|
|
];
|
|
|
|
const callExpressionMultiDefinitions = [
|
|
"ChromeUtils.defineESModuleGetters(lazy,",
|
|
"XPCOMUtils.defineLazyServiceGetters(lazy,",
|
|
"Object.defineProperties(lazy,",
|
|
"loader.lazyRequireGetter(lazy,",
|
|
];
|
|
|
|
module.exports = {
|
|
meta: {
|
|
docs: {
|
|
url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/valid-lazy.html",
|
|
},
|
|
messages: {
|
|
duplicateSymbol: "Duplicate symbol {{name}} being added to lazy.",
|
|
incorrectType: "Unexpected literal for property name {{name}}",
|
|
unknownProperty: "Unknown lazy member property {{name}}",
|
|
unusedProperty: "Unused lazy property {{name}}",
|
|
topLevelAndUnconditional:
|
|
"Lazy property {{name}} is used at top-level unconditionally. It should be non-lazy.",
|
|
},
|
|
schema: [],
|
|
type: "problem",
|
|
},
|
|
|
|
create(context) {
|
|
let lazyProperties = new Map();
|
|
let unknownProperties = [];
|
|
let isLazyExported = false;
|
|
|
|
function getAncestorNodes(node) {
|
|
const ancestors = [];
|
|
node = node.parent;
|
|
while (node) {
|
|
ancestors.unshift(node);
|
|
node = node.parent;
|
|
}
|
|
return ancestors;
|
|
}
|
|
|
|
// Returns true if lazy getter definitions in prevNode and currNode are
|
|
// duplicate.
|
|
// This returns false if prevNode and currNode have the same IfStatement as
|
|
// ancestor and they're in different branches.
|
|
function isDuplicate(prevNode, currNode) {
|
|
const prevAncestors = getAncestorNodes(prevNode);
|
|
const currAncestors = getAncestorNodes(currNode);
|
|
|
|
for (
|
|
let i = 0;
|
|
i < prevAncestors.length && i < currAncestors.length;
|
|
i++
|
|
) {
|
|
const prev = prevAncestors[i];
|
|
const curr = currAncestors[i];
|
|
if (prev === curr && prev.type === "IfStatement") {
|
|
if (prevAncestors[i + 1] !== currAncestors[i + 1]) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function addProp(callNode, propNode, name) {
|
|
if (
|
|
lazyProperties.has(name) &&
|
|
isDuplicate(lazyProperties.get(name).callNode, callNode)
|
|
) {
|
|
context.report({
|
|
node: propNode,
|
|
messageId: "duplicateSymbol",
|
|
data: { name },
|
|
});
|
|
return;
|
|
}
|
|
lazyProperties.set(name, { used: false, callNode, propNode });
|
|
}
|
|
|
|
function setPropertiesFromArgument(callNode, arg) {
|
|
if (arg.type === "ObjectExpression") {
|
|
for (let propNode of arg.properties) {
|
|
if (propNode.key.type == "Literal") {
|
|
context.report({
|
|
node: propNode,
|
|
messageId: "incorrectType",
|
|
data: { name: propNode.key.value },
|
|
});
|
|
continue;
|
|
}
|
|
addProp(callNode, propNode, propNode.key.name);
|
|
}
|
|
} else if (arg.type === "ArrayExpression") {
|
|
for (let propNode of arg.elements) {
|
|
if (propNode.type != "Literal") {
|
|
continue;
|
|
}
|
|
addProp(callNode, propNode, propNode.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
VariableDeclarator(node) {
|
|
if (
|
|
node.id.type === "Identifier" &&
|
|
node.id.name == "lazy" &&
|
|
node.init.type == "CallExpression" &&
|
|
node.init.callee.name == "createLazyLoaders" &&
|
|
node.init.arguments.length >= 1
|
|
) {
|
|
setPropertiesFromArgument(node.init, node.init.arguments[0]);
|
|
}
|
|
},
|
|
|
|
CallExpression(node) {
|
|
if (
|
|
node.callee.type != "MemberExpression" ||
|
|
(node.callee.object.type == "MemberExpression" &&
|
|
!items.includes(node.callee.object.object.name)) ||
|
|
(node.callee.object.type != "MemberExpression" &&
|
|
!items.includes(node.callee.object.name))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let source;
|
|
try {
|
|
source = helpers.getASTSource(node);
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
|
|
for (let reg of callExpressionDefinitions) {
|
|
let match = source.match(reg);
|
|
if (match) {
|
|
if (
|
|
lazyProperties.has(match[1]) &&
|
|
isDuplicate(lazyProperties.get(match[1]).callNode, node)
|
|
) {
|
|
context.report({
|
|
node,
|
|
messageId: "duplicateSymbol",
|
|
data: { name: match[1] },
|
|
});
|
|
return;
|
|
}
|
|
lazyProperties.set(match[1], {
|
|
used: false,
|
|
callNode: node,
|
|
propNode: node,
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (
|
|
callExpressionMultiDefinitions.some(expr =>
|
|
source.startsWith(expr)
|
|
) &&
|
|
node.arguments[1]
|
|
) {
|
|
setPropertiesFromArgument(node, node.arguments[1]);
|
|
}
|
|
},
|
|
|
|
MemberExpression(node) {
|
|
if (node.computed || node.object.type !== "Identifier") {
|
|
return;
|
|
}
|
|
|
|
let name;
|
|
if (node.object.name == "lazy") {
|
|
name = node.property.name;
|
|
} else {
|
|
return;
|
|
}
|
|
let property = lazyProperties.get(name);
|
|
if (!property) {
|
|
// These will be reported on Program:exit - some definitions may
|
|
// be after first use, so we need to wait until we've processed
|
|
// the whole file before reporting.
|
|
unknownProperties.push({ name, node });
|
|
} else {
|
|
property.used = true;
|
|
}
|
|
if (
|
|
helpers.getIsTopLevelAndUnconditionallyExecuted(
|
|
context.sourceCode.getAncestors(node)
|
|
)
|
|
) {
|
|
context.report({
|
|
node,
|
|
messageId: "topLevelAndUnconditional",
|
|
data: { name },
|
|
});
|
|
}
|
|
},
|
|
|
|
ExportNamedDeclaration(node) {
|
|
for (const spec of node.specifiers) {
|
|
if (spec.local.name === "lazy") {
|
|
// If the lazy object is exported, do not check unused property.
|
|
isLazyExported = true;
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
"Program:exit": function () {
|
|
for (let { name, node } of unknownProperties) {
|
|
let property = lazyProperties.get(name);
|
|
if (!property) {
|
|
context.report({
|
|
node,
|
|
messageId: "unknownProperty",
|
|
data: { name },
|
|
});
|
|
} else {
|
|
property.used = true;
|
|
}
|
|
}
|
|
if (!isLazyExported) {
|
|
for (let [name, property] of lazyProperties.entries()) {
|
|
if (!property.used) {
|
|
context.report({
|
|
node: property.propNode,
|
|
messageId: "unusedProperty",
|
|
data: { name },
|
|
});
|
|
}
|
|
}
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|