@locker/eslint-plugin-unsafe-types
Version:
Detect usage of unsigned unsafe types.
569 lines (512 loc) • 20 kB
JavaScript
// Original source code https://github.com/eslint/eslint/blob/main/lib/rules/utils/ast-utils.js
const anyFunctionPattern = /^(?:Function(?:Declaration|Expression)|ArrowFunctionExpression)$/u;
const arrayMethodWithThisArgPattern =
/^(?:every|filter|find(?:Last)?(?:Index)?|flatMap|forEach|map|some)$/u;
const arrayOrTypedArrayPattern = /Array$/u;
const bindOrCallOrApplyPattern = /^(?:bind|call|apply)$/u;
const thisTagPattern = /^[\s*]*/mu;
const signatures = new Set([
'$A.lockerService.restricted.createScript',
'$A.$lockerService$.$restricted$.$createScript$',
'$A.lockerService.trusted.createScript',
'$A.$lockerService$.$trusted$.$createScript$',
'trusted.createScript',
]);
/**
* Check if the `actual` is an expected value.
* @param {string} actual The string value to check.
* @param {string | RegExp} expected The expected string value or pattern.
* @returns {boolean} `true` if the `actual` is an expected value.
*/
function checkText(actual, expected) {
return typeof expected === 'string' ? actual === expected : expected.test(actual);
}
/**
* Gets the property name of a given node.
* The node can be a MemberExpression, a Property, or a MethodDefinition.
*
* If the name is dynamic, this returns `null`.
*
* For examples:
*
* a.b // => "b"
* a["b"] // => "b"
* a['b'] // => "b"
* a[`b`] // => "b"
* a[100] // => "100"
* a[b] // => null
* a["a" + "b"] // => null
* a[tag`b`] // => null
* a[`${b}`] // => null
*
* let a = {b: 1} // => "b"
* let a = {["b"]: 1} // => "b"
* let a = {['b']: 1} // => "b"
* let a = {[`b`]: 1} // => "b"
* let a = {[100]: 1} // => "100"
* let a = {[b]: 1} // => null
* let a = {["a" + "b"]: 1} // => null
* let a = {[tag`b`]: 1} // => null
* let a = {[`${b}`]: 1} // => null
* @param {ASTNode} node The node to get.
* @returns {string|null} The property name if static. Otherwise, null.
*/
function getStaticPropertyName(node) {
let prop;
switch (node && node.type) {
case 'ChainExpression':
return getStaticPropertyName(node.expression);
case 'Property':
case 'PropertyDefinition':
case 'MethodDefinition':
prop = node.key;
break;
case 'MemberExpression':
prop = node.property;
break;
// no default
}
if (prop) {
if (prop.type === 'Identifier' && !node.computed) {
return prop.name;
}
return getStaticStringValue(prop);
}
return null;
}
/**
* Returns the result of the string conversion applied to the evaluated value of the given expression node,
* if it can be determined statically.
*
* This function returns a `string` value for all `Literal` nodes and simple `TemplateLiteral` nodes only.
* In all other cases, this function returns `null`.
* @param {ASTNode} node Expression node.
* @returns {string|null} String value if it can be determined. Otherwise, `null`.
*/
function getStaticStringValue(node) {
switch (node.type) {
case 'Literal':
if (node.value === null) {
if (isNullLiteral(node)) {
return String(node.value); // "null"
}
if (node.regex) {
return `/${node.regex.pattern}/${node.regex.flags}`;
}
if (node.bigint) {
return node.bigint;
}
// Otherwise, this is an unknown literal. The function will return null.
} else {
return String(node.value);
}
break;
case 'TemplateLiteral':
if (node.expressions.length === 0 && node.quasis.length === 1) {
return node.quasis[0].value.cooked;
}
break;
// no default
}
return null;
}
/**
* Finds a function node from ancestors of a node.
* @param {ASTNode} node A start node to find.
* @returns {Node|null} A found function node.
*/
function getUpperFunction(node) {
for (let currentNode = node; currentNode; currentNode = currentNode.parent) {
if (anyFunctionPattern.test(currentNode.type)) {
return currentNode;
}
}
return null;
}
/**
* Finds the variable by a given name in a given scope and its upper scopes.
* @param {eslint-scope.Scope} initScope A scope to start find.
* @param {string} name A variable name to find.
* @returns {eslint-scope.Variable|null} A found variable or `null`.
*/
function getVariableByName(initScope, name) {
let scope = initScope;
while (scope) {
const variable = scope.set.get(name);
if (variable) {
return variable;
}
scope = scope.upper;
}
return null;
}
/**
* Checks whether or not a node has a `@this` tag in its comments.
* @param {ASTNode} node A node to check.
* @param {SourceCode} sourceCode A SourceCode instance to get comments.
* @returns {boolean} Whether or not the node has a `@this` tag in its comments.
*/
function hasJSDocThisTag(node, sourceCode) {
const jsdocComment = sourceCode.getJSDocComment(node);
if (jsdocComment && thisTagPattern.test(jsdocComment.value)) {
return true;
}
// Checks `@this` in its leading comments for callbacks,
// because callbacks don't have its JSDoc comment.
// e.g.
// sinon.test(/* @this sinon.Sandbox */function() { this.spy(); });
return sourceCode.getCommentsBefore(node).some((comment) => thisTagPattern.test(comment.value));
}
/**
* Checks whether or not a node is `Array.from`.
* @param {ASTNode} node A node to check.
* @returns {boolean} Whether or not the node is a `Array.from`.
*/
function isArrayFromMethod(node) {
return isSpecificMemberAccess(node, arrayOrTypedArrayPattern, 'from');
}
/**
* Checks whether or not a node is callee.
* @param {ASTNode} node A node to check.
* @returns {boolean} Whether or not the node is callee.
*/
function isCallee(node) {
return node.parent.type === 'CallExpression' && node.parent.callee === node;
}
/**
* Checks whether or not a given function node is the default `this` binding.
*
* First, this checks the node:
*
* - The given node is not in `PropertyDefinition#value` position.
* - The given node is not `StaticBlock`.
* - The function name does not start with uppercase. It's a convention to capitalize the names
* of constructor functions. This check is not performed if `capIsConstructor` is set to `false`.
* - The function does not have a JSDoc comment that has a @this tag.
*
* Next, this checks the location of the node.
* If the location is below, this judges `this` is valid.
*
* - The location is not on an object literal.
* - The location is not assigned to a variable which starts with an uppercase letter. Applies to anonymous
* functions only, as the name of the variable is considered to be the name of the function in this case.
* This check is not performed if `capIsConstructor` is set to `false`.
* - The location is not on an ES2015 class.
* - Its `bind`/`call`/`apply` method is not called directly.
* - The function is not a callback of array methods (such as `.forEach()`) if `thisArg` is given.
* @param {ASTNode} node A function node to check. It also can be an implicit function, like `StaticBlock`
* or any expression that is `PropertyDefinition#value` node.
* @param {SourceCode} sourceCode A SourceCode instance to get comments.
* @param {boolean} [capIsConstructor = true] `false` disables the assumption that functions which name starts
* with an uppercase or are assigned to a variable which name starts with an uppercase are constructors.
* @returns {boolean} The function node is the default `this` binding.
*/
function isDefaultThisBinding(node, sourceCode, { capIsConstructor = true } = {}) {
/*
* Class field initializers are implicit functions, but ESTree doesn't have the AST node of field initializers.
* Therefore, A expression node at `PropertyDefinition#value` is a function.
* In this case, `this` is always not default binding.
*/
if (node.parent.type === 'PropertyDefinition' && node.parent.value === node) {
return false;
}
// Class static blocks are implicit functions. In this case, `this` is always not default binding.
if (node.type === 'StaticBlock') {
return false;
}
if ((capIsConstructor && isES5Constructor(node)) || hasJSDocThisTag(node, sourceCode)) {
return false;
}
const isAnonymous = node.id === null;
let currentNode = node;
while (currentNode) {
const parent = currentNode.parent;
switch (parent.type) {
/*
* Looks up the destination.
* e.g., obj.foo = nativeFoo || function foo() { ... };
*/
case 'LogicalExpression':
case 'ConditionalExpression':
case 'ChainExpression':
currentNode = parent;
break;
/*
* If the upper function is IIFE, checks the destination of the return value.
* e.g.
* obj.foo = (function() {
* // setup...
* return function foo() { ... };
* })();
* obj.foo = (() =>
* function foo() { ... }
* )();
*/
case 'ReturnStatement': {
const func = getUpperFunction(parent);
if (func === null || !isCallee(func)) {
return true;
}
currentNode = func.parent;
break;
}
case 'ArrowFunctionExpression':
if (currentNode !== parent.body || !isCallee(parent)) {
return true;
}
currentNode = parent.parent;
break;
/*
* e.g.
* var obj = { foo() { ... } };
* var obj = { foo: function() { ... } };
* class A { constructor() { ... } }
* class A { foo() { ... } }
* class A { get foo() { ... } }
* class A { set foo() { ... } }
* class A { static foo() { ... } }
* class A { foo = function() { ... } }
*/
case 'Property':
case 'PropertyDefinition':
case 'MethodDefinition':
return parent.value !== currentNode;
/*
* e.g.
* obj.foo = function foo() { ... };
* Foo = function() { ... };
* [obj.foo = function foo() { ... }] = a;
* [Foo = function() { ... }] = a;
*/
case 'AssignmentExpression':
case 'AssignmentPattern':
if (parent.left.type === 'MemberExpression') {
return false;
}
if (
capIsConstructor &&
isAnonymous &&
parent.left.type === 'Identifier' &&
startsWithUpperCase(parent.left.name)
) {
return false;
}
return true;
/*
* e.g.
* var Foo = function() { ... };
*/
case 'VariableDeclarator':
return !(
capIsConstructor &&
isAnonymous &&
parent.init === currentNode &&
parent.id.type === 'Identifier' &&
startsWithUpperCase(parent.id.name)
);
/*
* e.g.
* var foo = function foo() { ... }.bind(obj);
* (function foo() { ... }).call(obj);
* (function foo() { ... }).apply(obj, []);
*/
case 'MemberExpression':
if (
parent.object === currentNode &&
isSpecificMemberAccess(parent, null, bindOrCallOrApplyPattern)
) {
const maybeCalleeNode =
parent.parent.type === 'ChainExpression' ? parent.parent : parent;
return !(
isCallee(maybeCalleeNode) &&
maybeCalleeNode.parent.arguments.length >= 1 &&
!isNullOrUndefined(maybeCalleeNode.parent.arguments[0])
);
}
return true;
/*
* e.g.
* Reflect.apply(function() {}, obj, []);
* Array.from([], function() {}, obj);
* list.forEach(function() {}, obj);
*/
case 'CallExpression':
if (isReflectApply(parent.callee)) {
return (
parent.arguments.length !== 3 ||
parent.arguments[0] !== currentNode ||
isNullOrUndefined(parent.arguments[1])
);
}
if (isArrayFromMethod(parent.callee)) {
return (
parent.arguments.length !== 3 ||
parent.arguments[1] !== currentNode ||
isNullOrUndefined(parent.arguments[2])
);
}
if (isMethodWhichHasThisArg(parent.callee)) {
return (
parent.arguments.length !== 2 ||
parent.arguments[0] !== currentNode ||
isNullOrUndefined(parent.arguments[1])
);
}
return true;
// Otherwise `this` is default.
default:
return true;
}
}
/* c8 ignore next */
return true;
}
/**
* Checks whether or not a node is a constructor.
* @param {ASTNode} node A function node to check.
* @returns {boolean} Whether or not a node is a constructor.
*/
function isES5Constructor(node) {
return node.id && startsWithUpperCase(node.id.name);
}
/**
* Checks whether or not a node is a method which expects a function as a first argument, and `thisArg` as a second argument.
* @param {ASTNode} node A node to check.
* @returns {boolean} Whether or not the node is a method which expects a function as a first argument, and `thisArg` as a second argument.
*/
function isMethodWhichHasThisArg(node) {
return isSpecificMemberAccess(node, null, arrayMethodWithThisArgPattern);
}
/**
* Determines whether the given node is a `null` literal.
* @param {ASTNode} node The node to check
* @returns {boolean} `true` if the node is a `null` literal
*/
function isNullLiteral(node) {
/*
* Checking `node.value === null` does not guarantee that a literal is a null literal.
* When parsing values that cannot be represented in the current environment (e.g. unicode
* regexes in Node 4), `node.value` is set to `null` because it wouldn't be possible to
* set `node.value` to a unicode regex. To make sure a literal is actually `null`, check
* `node.regex` instead. Also see: https://github.com/eslint/eslint/issues/8020
*/
return node.type === 'Literal' && node.value === null && !node.regex && !node.bigint;
}
/**
* Checks whether or not a node is `null` or `undefined`.
* @param {ASTNode} node A node to check.
* @returns {boolean} Whether or not the node is a `null` or `undefined`.
* @public
*/
function isNullOrUndefined(node) {
return (
isNullLiteral(node) ||
(node.type === 'Identifier' && node.name === 'undefined') ||
(node.type === 'UnaryExpression' && node.operator === 'void')
);
}
/**
* Checks whether or not a node is `Reflect.apply`.
* @param {ASTNode} node A node to check.
* @returns {boolean} Whether or not the node is a `Reflect.apply`.
*/
function isReflectApply(node) {
return isSpecificMemberAccess(node, 'Reflect', 'apply');
}
// LWS BEGIN
/**
* Checks whether the node is a call to LWS signing API.
* ES6 modules need to pass isSigningApiImported.
* @param {ASTNode} node
* @param {Set} signatures
* @returns {boolean} Whether the node is the signing API.
*/
function isSignatureCall(node) {
if (node.type === 'CallExpression') {
// Check if the API is signed using $A utilities
if (node.callee.type === 'MemberExpression') {
const memberExpr = node.callee;
// Do not allow computed access like $A['lockerService']... syntax
if (memberExpr.isComputed) {
return false;
}
const memberExprParts = [memberExpr.property.name];
let object = memberExpr.object;
while (object.type === 'MemberExpression') {
memberExprParts.push(object.property.name);
object = object.object;
}
memberExprParts.push(object.name);
const memberExprString = memberExprParts.reverse().join('.');
return signatures.has(memberExprString);
}
}
return false;
}
// LWS END
/**
* Check if a given node is an Identifier node with a given name.
* @param {ASTNode} node The node to check.
* @param {string | RegExp} name The expected name or the expected pattern of the object name.
* @returns {boolean} `true` if the node is an Identifier node with the name.
*/
function isSpecificId(node, name) {
return node.type === 'Identifier' && checkText(node.name, name);
}
/**
* Check if a given node is member access with a given object name and property name pair.
* This is regardless of optional or not.
* @param {ASTNode} node The node to check.
* @param {string | RegExp | null} objectName The expected name or the expected pattern of the object name. If this is nullish, this method doesn't check object.
* @param {string | RegExp | null} propertyName The expected name or the expected pattern of the property name. If this is nullish, this method doesn't check property.
* @returns {boolean} `true` if the node is member access with the object name and property name pair.
* The node is a `MemberExpression` or `ChainExpression`.
*/
function isSpecificMemberAccess(node, objectName, propertyName) {
const checkNode = skipChainExpression(node);
if (checkNode.type !== 'MemberExpression') {
return false;
}
if (objectName && !isSpecificId(checkNode.object, objectName)) {
return false;
}
if (propertyName) {
const actualPropertyName = getStaticPropertyName(checkNode);
if (
typeof actualPropertyName !== 'string' ||
!checkText(actualPropertyName, propertyName)
) {
return false;
}
}
return true;
}
/**
* Checks whether the given string starts with uppercase or not.
* @param {string} s The string to check.
* @returns {boolean} `true` if the string starts with uppercase.
*/
function startsWithUpperCase(s) {
return s[0] !== s[0].toLocaleLowerCase();
}
/**
* Retrieve `ChainExpression#expression` value if the given node a `ChainExpression` node. Otherwise, pass through it.
* @param {ASTNode} node The node to address.
* @returns {ASTNode} The `ChainExpression#expression` value if the node is a `ChainExpression` node. Otherwise, the node.
*/
function skipChainExpression(node) {
return node && node.type === 'ChainExpression' ? node.expression : node;
}
module.exports = {
checkText,
getStaticPropertyName,
getStaticStringValue,
getVariableByName,
isCallee,
isDefaultThisBinding,
isNullLiteral,
isSignatureCall,
isSpecificId,
isSpecificMemberAccess,
skipChainExpression,
};