Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fix no-restricted-properties false negatives with unknown objects #17818

Merged
merged 8 commits into from Dec 14, 2023
108 changes: 83 additions & 25 deletions lib/rules/no-restricted-properties.js
Expand Up @@ -101,6 +101,25 @@ module.exports = {
}
});

/**
* gets the properties which are present in both the map of restricted properties(global or with an object) and in the parsed source code
* @param {any} restricted they can be either globally restricted or passed with objects
* @param {string[]} propertyNames the list of properties used in the code
* @returns {(boolean | { name: string, message: string | undefined }[])} the list of matched properties with their error / warning messages
*/
function getMatchingProperties(restricted, propertyNames) {
const matchedProperties = [];

propertyNames.forEach(property => {
if (!restricted.has(property)) {
return;
}
matchedProperties.push({ name: property, ...restricted.get(property) });
});

return matchedProperties.length === 0 ? false : matchedProperties;
}

/**
* Checks to see whether a property access is restricted, and reports it if so.
* @param {ASTNode} node The node to report
Expand All @@ -113,29 +132,46 @@ module.exports = {
return;
}
const matchedObject = restrictedProperties.get(objectName);
const matchedObjectProperty = matchedObject ? matchedObject.get(propertyName) : globallyRestrictedObjects.get(objectName);
const globalMatchedProperty = globallyRestrictedProperties.get(propertyName);
const matchedObjectProperty = matchedObject ? getMatchingProperties(matchedObject, propertyName) : false;
const globalMatchedObject = globallyRestrictedObjects.get(objectName);
const globalMatchedProperty = getMatchingProperties(globallyRestrictedProperties, propertyName);

if (matchedObjectProperty) {
const message = matchedObjectProperty.message ? ` ${matchedObjectProperty.message}` : "";
for (const props of matchedObjectProperty) {
const message = props.message ? ` ${props.message}` : "";

context.report({
node,
messageId: "restrictedObjectProperty",
data: {
objectName,
propertyName: props.name,
message
}
});
}
} else if (globalMatchedProperty) {
for (const props of globalMatchedProperty) {
const message = props.message ? ` ${props.message}` : "";

context.report({
node,
messageId: "restrictedProperty",
data: {
propertyName: props.name,
message
}
});
}
} else if (globalMatchedObject) {
const message = globalMatchedObject.message ? ` ${globalMatchedObject.message}` : "";

context.report({
node,
messageId: "restrictedObjectProperty",
data: {
objectName,
propertyName,
message
}
});
} else if (globalMatchedProperty) {
const message = globalMatchedProperty.message ? ` ${globalMatchedProperty.message}` : "";

context.report({
node,
messageId: "restrictedProperty",
data: {
propertyName,
propertyName: propertyName[0],
message
}
});
Expand All @@ -148,34 +184,56 @@ module.exports = {
* @returns {undefined}
*/
function checkDestructuringAssignment(node) {
if (node.right.type === "Identifier") {
const objectName = node.right.name;
if (node.right.type === "Identifier" || node.right.type === "CallExpression") {
const objectName = node.right.type === "Identifier" ? node.right.name : "";

if (node.left.type === "ObjectPattern") {
node.left.properties.forEach(property => {
checkPropertyAccess(node.left, objectName, astUtils.getStaticPropertyName(property));
checkPropertyAccess(node.left, objectName, astUtils.getAllProperties(node.left.properties));
}
if (node.left.type === "ArrayPattern") {
node.left.elements.forEach(element => {
if (element && element.type === "ObjectPattern") {
checkPropertyAccess(element, objectName, astUtils.getAllProperties(element.properties));
}
});
}
}
}

return {
MemberExpression(node) {
checkPropertyAccess(node, node.object && node.object.name, astUtils.getStaticPropertyName(node));
checkPropertyAccess(node, node.object && node.object.name, [astUtils.getStaticPropertyName(node)]);
},
VariableDeclarator(node) {
if (node.init && node.init.type === "Identifier") {
const objectName = node.init.name;
if (node.init && (node.init.type === "Identifier" || node.init.type === "CallExpression")) {
const objectName = node.init.type === "Identifier" ? node.init.name : "";

if (node.id.type === "ObjectPattern") {
node.id.properties.forEach(property => {
checkPropertyAccess(node.id, objectName, astUtils.getStaticPropertyName(property));
checkPropertyAccess(node.id, objectName, astUtils.getAllProperties(node.id.properties));
mdjermanovic marked this conversation as resolved.
Show resolved Hide resolved
}

if (node.id.type === "ArrayPattern") {
node.id.elements.forEach(element => {
if (element && element.type === "ObjectPattern") {
checkPropertyAccess(element, objectName, astUtils.getAllProperties(element.properties));
}
});
}
}
},
AssignmentExpression: checkDestructuringAssignment,
AssignmentPattern: checkDestructuringAssignment
AssignmentPattern: checkDestructuringAssignment,
ArrowFunctionExpression(node) {
for (const param of node.params) {
if (param.type === "ObjectPattern") {
checkPropertyAccess(param, "", astUtils.getAllProperties(param.properties));
} else if (param.type === "AssignmentPattern" && param.left.type === "ObjectPattern") {
const objectName = param.right.type === "Identifier" ? param.right.name : "";

checkPropertyAccess(param, objectName, astUtils.getAllProperties(param.left.properties));
}
}
}
};
}
};
46 changes: 46 additions & 0 deletions lib/rules/utils/ast-utils.js
Expand Up @@ -291,6 +291,51 @@ function getStaticPropertyName(node) {
return null;
}


/**
* Gets all the properties used in object destructuring at a given node.
* The node can only be a Property.
* For examples :-
* { a } = m // => [ "a" ]
* { a, b } = m // => [ "a", "b" ]
* { a: { c } = {} } = m // => ["a", "c"]
* { a: { b: c, d }, k } = m // => ["a", "b", "d", "k"]
* @param {ASTNode[]} propArray the list of properties which are passed
* @returns {string[]} the list of all properties which are used
*/
function getAllProperties(propArray) {
const propsUsed = [];

/**
* Collects the properties used in an array.
* For example - if the given code is
* { a, c: { d, x: { z } },v }
* it get stored in the array as [ "a", "c", "d", "x", "z", "v" ]
* @param {ASTNode[]} properties the array of nodes
* @returns {undefined}
*/
function traverse(properties) {
for (const node of properties) {
if (node.type !== "Property") {
return;
}
if (node.key.type === "Identifier" && !node.computed) {
propsUsed.push(node.key.name);
} else {
propsUsed.push(getStaticStringValue(node.key));
}
if (node.value.type === "ObjectPattern") {
traverse(node.value.properties);
}
if (node.value.type === "AssignmentPattern" && node.value.left.type === "ObjectPattern") {
traverse(node.value.left.properties);
}
}
}
traverse(propArray);
return propsUsed;
}

/**
* Retrieve `ChainExpression#expression` value if the given node a `ChainExpression` node. Otherwise, pass through it.
* @param {ASTNode} node The node to address.
Expand Down Expand Up @@ -2266,6 +2311,7 @@ module.exports = {
isNullLiteral,
getStaticStringValue,
getStaticPropertyName,
getAllProperties,
skipChainExpression,
isSpecificId,
isSpecificMemberAccess,
Expand Down
136 changes: 132 additions & 4 deletions tests/lib/rules/no-restricted-properties.js
Expand Up @@ -113,10 +113,6 @@ ruleTester.run("no-restricted-properties", rule, {
code: "let {unrelated} = foo;",
options: [{ object: "foo", property: "bar" }],
parserOptions: { ecmaVersion: 6 }
}, {
code: "let {baz: {bar: qux}} = foo;",
options: [{ object: "foo", property: "bar" }],
parserOptions: { ecmaVersion: 6 }
mdjermanovic marked this conversation as resolved.
Show resolved Hide resolved
}, {
code: "let {bar} = foo.baz;",
options: [{ object: "foo", property: "bar" }],
Expand Down Expand Up @@ -546,6 +542,138 @@ ruleTester.run("no-restricted-properties", rule, {
},
type: "MemberExpression"
}]
}, {
code: "const { bar: { bad } = {} } = foo;",
options: [{ property: "bad" }],
parserOptions: { ecmaVersion: 6 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parserOptions: { ecmaVersion: 6 },
languageOptions: { ecmaVersion: 6 },

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did, ig the ci will fail since the base is an older commit, anything that I should do to fix it ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI checks out from a merge commit, so the tests should pass.

Problem is only linting, as languageOptions should be at the same place where parserOptions were. If you could update that, it would be great.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem is only linting, as languageOptions should be at the same place where parserOptions were. If you could update that, it would be great.

2023-12-14T12:03:14.9805284Z /home/runner/work/eslint/eslint/tests/lib/rules/no-restricted-properties.js
2023-12-14T12:03:14.9909087Z ##[error]  559:13  error  The properties of a test case should be placed in a consistent order: [code, options, languageOptions, errors]  eslint-plugin/test-case-property-ordering
2023-12-14T12:03:14.9922248Z ##[error]  570:13  error  The properties of a test case should be placed in a consistent order: [code, options, languageOptions, errors]  eslint-plugin/test-case-property-ordering
2023-12-14T12:03:14.9927445Z ##[error]  581:13  error  The properties of a test case should be placed in a consistent order: [code, options, languageOptions, errors]  eslint-plugin/test-case-property-ordering
2023-12-14T12:03:14.9932354Z ##[error]  592:13  error  The properties of a test case should be placed in a consistent order: [code, options, languageOptions, errors]  eslint-plugin/test-case-property-ordering
2023-12-14T12:03:14.9937079Z ##[error]  603:13  error  The properties of a test case should be placed in a consistent order: [code, options, languageOptions, errors]  eslint-plugin/test-case-property-ordering
2023-12-14T12:03:14.9941897Z ##[error]  614:13  error  The properties of a test case should be placed in a consistent order: [code, options, languageOptions, errors]  eslint-plugin/test-case-property-ordering
2023-12-14T12:03:14.9946759Z ##[error]  625:13  error  The properties of a test case should be placed in a consistent order: [code, options, languageOptions, errors]  eslint-plugin/test-case-property-ordering
2023-12-14T12:03:14.9952174Z ##[error]  636:13  error  The properties of a test case should be placed in a consistent order: [code, options, languageOptions, errors]  eslint-plugin/test-case-property-ordering
2023-12-14T12:03:14.9957490Z ##[error]  647:13  error  The properties of a test case should be placed in a consistent order: [code, options, languageOptions, errors]  eslint-plugin/test-case-property-ordering
2023-12-14T12:03:14.9962312Z ##[error]  658:13  error  The properties of a test case should be placed in a consistent order: [code, options, languageOptions, errors]  eslint-plugin/test-case-property-ordering
2023-12-14T12:03:14.9967210Z ##[error]  669:13  error  The properties of a test case should be placed in a consistent order: [code, options, languageOptions, errors]  eslint-plugin/test-case-property-ordering
2023-12-14T12:03:14.9972095Z ##[error]  680:13  error  The properties of a test case should be placed in a consistent order: [code, options, languageOptions, errors]  eslint-plugin/test-case-property-ordering

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ye, done.

errors: [{
messageId: "restrictedProperty",
data: {
propertyName: "bad",
message: ""
}
}]
}, {
code: "const { bar: { bad } } = foo;",
options: [{ property: "bad" }],
parserOptions: { ecmaVersion: 6 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parserOptions: { ecmaVersion: 6 },
languageOptions: { ecmaVersion: 6 },

errors: [{
messageId: "restrictedProperty",
data: {
propertyName: "bad",
message: ""
}
}]
}, {
code: "const { bad } = foo();",
options: [{ property: "bad" }],
parserOptions: { ecmaVersion: 6 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parserOptions: { ecmaVersion: 6 },
languageOptions: { ecmaVersion: 6 },

errors: [{
messageId: "restrictedProperty",
data: {
propertyName: "bad",
message: ""
}
}]
}, {
code: "({ bad } = foo());",
options: [{ property: "bad" }],
parserOptions: { ecmaVersion: 6 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parserOptions: { ecmaVersion: 6 },
languageOptions: { ecmaVersion: 6 },

errors: [{
messageId: "restrictedProperty",
data: {
propertyName: "bad",
message: ""
}
}]
}, {
code: "({ bar: { bad } } = foo);",
options: [{ property: "bad" }],
parserOptions: { ecmaVersion: 6 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parserOptions: { ecmaVersion: 6 },
languageOptions: { ecmaVersion: 6 },

errors: [{
messageId: "restrictedProperty",
data: {
propertyName: "bad",
message: ""
}
}]
}, {
code: "({ bar: { bad } = {} } = foo);",
options: [{ property: "bad" }],
parserOptions: { ecmaVersion: 6 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parserOptions: { ecmaVersion: 6 },
languageOptions: { ecmaVersion: 6 },

errors: [{
messageId: "restrictedProperty",
data: {
propertyName: "bad",
message: ""
}
}]
}, {
code: "({ bad }) => {};",
options: [{ property: "bad" }],
parserOptions: { ecmaVersion: 6 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parserOptions: { ecmaVersion: 6 },
languageOptions: { ecmaVersion: 6 },

errors: [{
messageId: "restrictedProperty",
data: {
propertyName: "bad",
message: ""
}
}]
}, {
code: "({ bad } = {}) => {};",
options: [{ property: "bad" }],
parserOptions: { ecmaVersion: 6 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parserOptions: { ecmaVersion: 6 },
languageOptions: { ecmaVersion: 6 },

errors: [{
messageId: "restrictedProperty",
data: {
propertyName: "bad",
message: ""
}
}]
}, {
code: "({ bad: bar }) => {};",
options: [{ property: "bad" }],
parserOptions: { ecmaVersion: 6 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parserOptions: { ecmaVersion: 6 },
languageOptions: { ecmaVersion: 6 },

errors: [{
messageId: "restrictedProperty",
data: {
propertyName: "bad",
message: ""
}
}]
}, {
code: "({ bar: { bad } = {} }) => {};",
options: [{ property: "bad" }],
parserOptions: { ecmaVersion: 6 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parserOptions: { ecmaVersion: 6 },
languageOptions: { ecmaVersion: 6 },

errors: [{
messageId: "restrictedProperty",
data: {
propertyName: "bad",
message: ""
}
}]
}, {
code: "[{ bad }] = foo;",
options: [{ property: "bad" }],
parserOptions: { ecmaVersion: 6 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parserOptions: { ecmaVersion: 6 },
languageOptions: { ecmaVersion: 6 },

errors: [{
messageId: "restrictedProperty",
data: {
propertyName: "bad",
message: ""
}
}]
}, {
code: "const [{ bad }] = foo;",
options: [{ property: "bad" }],
parserOptions: { ecmaVersion: 6 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parserOptions: { ecmaVersion: 6 },
languageOptions: { ecmaVersion: 6 },

errors: [{
messageId: "restrictedProperty",
data: {
propertyName: "bad",
message: ""
}
}]
}
]
});