Skip to content

Commit 353dd37

Browse files
authored
Merge pull request #1194 from bmish/no-restricted-property-modifications
Add new rule `no-restricted-property-modifications`
2 parents 1f69e6a + f04a47f commit 353dd37

File tree

4 files changed

+449
-0
lines changed

4 files changed

+449
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ Rules are grouped by category to help you understand their purpose. Each rule ha
155155
| [no-html-safe](./docs/rules/no-html-safe.md) | disallow the use of `htmlSafe` | | |
156156
| [no-incorrect-calls-with-inline-anonymous-functions](./docs/rules/no-incorrect-calls-with-inline-anonymous-functions.md) | disallow inline anonymous functions as arguments to `debounce`, `once`, and `scheduleOnce` | :white_check_mark: | |
157157
| [no-invalid-debug-function-arguments](./docs/rules/no-invalid-debug-function-arguments.md) | disallow usages of Ember's `assert()` / `warn()` / `deprecate()` functions that have the arguments passed in the wrong order. | :white_check_mark: | |
158+
| [no-restricted-property-modifications](./docs/rules/no-restricted-property-modifications.md) | disallow modifying the specified properties | | :wrench: |
158159
| [require-fetch-import](./docs/rules/require-fetch-import.md) | enforce explicit import for `fetch()` | | |
159160

160161
### Routes
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# no-restricted-property-modifications
2+
3+
:wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
4+
5+
There are some properties, especially globally-injected ones, that you may want to treat as read-only, and ensure that no one modifies them.
6+
7+
## Rule Details
8+
9+
This rule prevents modifying the specified properties.
10+
11+
It also disallows using computed property macros like `alias` and `reads` that enable the specified properties to be indirectly modified.
12+
13+
## Examples
14+
15+
All examples assume a configuration of `properties: ['currentUser']`.
16+
17+
Examples of **incorrect** code for this rule:
18+
19+
```js
20+
import Component from '@ember/component';
21+
import { alias, reads } from '@ember/object/computed';
22+
23+
export default class MyComponent extends Component {
24+
@alias('currentUser') aliasForCurrentUser1; // Not allowed
25+
@reads('currentUser') aliasForCurrentUser2; // Not allowed
26+
27+
@alias('currentUser.somePermission1') somePermission1; // Not allowed
28+
@reads('currentUser.somePermission2') somePermission2; // Not allowed
29+
30+
myFunction() {
31+
this.set('currentUser', {}); // Not allowed
32+
this.set('currentUser.somePermission', true); // Not allowed
33+
34+
this.currentUser = {}; // Not allowed
35+
this.currentUser.somePermission = true; // Not allowed
36+
}
37+
}
38+
```
39+
40+
Examples of **correct** code for this rule:
41+
42+
```js
43+
import Component from '@ember/component';
44+
import { readOnly } from '@ember/object/computed';
45+
46+
export default class MyComponent extends Component {
47+
@readOnly('currentUser.somePermission') somePermission; // Allowed
48+
49+
myFunction() {
50+
console.log(this.currentUser.somePermission); // Allowed
51+
}
52+
}
53+
```
54+
55+
## Configuration
56+
57+
* object -- containing the following properties:
58+
* `String[]` -- `properties` -- array of names of properties that should not be modified (modifying child/nested/sub-properties of these is also not allowed)
59+
60+
Not yet implemented: There is currently no way to configure whether sub-properties are restricted from modification. To make this configurable, the `properties` array option could be updated to also accept objects of the form `{ name: 'myPropertyName', includeSubProperties: false }` where `includeSubProperties` defaults to `true`.
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
'use strict';
2+
3+
const { isStringLiteral } = require('../utils/types');
4+
const { getImportIdentifier } = require('../utils/import');
5+
const { isThisSet: isThisSetNative } = require('../utils/property-setter');
6+
const { nodeToDependentKey } = require('../utils/property-getter');
7+
8+
module.exports = {
9+
meta: {
10+
type: 'problem',
11+
docs: {
12+
description: 'disallow modifying the specified properties',
13+
category: 'Miscellaneous',
14+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-restricted-property-modifications.md',
15+
},
16+
fixable: 'code',
17+
schema: {
18+
type: 'array',
19+
minItems: 1,
20+
maxItems: 1,
21+
items: [
22+
{
23+
type: 'object',
24+
properties: {
25+
properties: {
26+
type: 'array',
27+
items: {
28+
type: 'string',
29+
},
30+
minItems: 1,
31+
uniqueItems: true,
32+
},
33+
},
34+
required: ['properties'],
35+
additionalProperties: false,
36+
},
37+
],
38+
},
39+
messages: {
40+
doNotUseAssignment: 'Do not use assignment on properties that should not be modified.',
41+
doNotUseSet: 'Do not call `set` on properties that should not be modified.',
42+
useReadOnlyMacro:
43+
'Use the `readOnly` computed property macro for properties that should not be modified.',
44+
},
45+
},
46+
create(context) {
47+
let importedComputedName;
48+
let importedReadsName;
49+
let importedAliasName;
50+
51+
const readOnlyProperties = context.options[0].properties;
52+
53+
return {
54+
ImportDeclaration(node) {
55+
if (node.source.value === '@ember/object') {
56+
importedComputedName =
57+
importedComputedName || getImportIdentifier(node, '@ember/object', 'computed');
58+
} else if (node.source.value === '@ember/object/computed') {
59+
importedReadsName =
60+
importedReadsName || getImportIdentifier(node, '@ember/object/computed', 'reads');
61+
importedAliasName =
62+
importedAliasName || getImportIdentifier(node, '@ember/object/computed', 'alias');
63+
}
64+
},
65+
66+
AssignmentExpression(node) {
67+
if (!isThisSetNative(node)) {
68+
return;
69+
}
70+
71+
const dependentKey = nodeToDependentKey(node.left, context);
72+
if (
73+
readOnlyProperties.includes(dependentKey) ||
74+
readOnlyProperties.some((property) => dependentKey.startsWith(`${property}.`))
75+
) {
76+
context.report({
77+
node,
78+
messageId: 'doNotUseAssignment',
79+
});
80+
}
81+
},
82+
83+
CallExpression(node) {
84+
if (
85+
isReadOnlyPropertyUsingAliasOrReads(
86+
node,
87+
readOnlyProperties,
88+
importedComputedName,
89+
importedAliasName,
90+
importedReadsName
91+
)
92+
) {
93+
context.report({
94+
node,
95+
messageId: 'useReadOnlyMacro',
96+
fix(fixer) {
97+
const argumentText0 = context.getSourceCode().getText(node.arguments[0]);
98+
return node.callee.type === 'MemberExpression'
99+
? fixer.replaceText(node, `${importedComputedName}.readOnly(${argumentText0})`)
100+
: fixer.replaceText(node, `readOnly(${argumentText0})`);
101+
},
102+
});
103+
} else if (isThisSetReadOnlyProperty(node, readOnlyProperties)) {
104+
context.report({ node, messageId: 'doNotUseSet' });
105+
}
106+
},
107+
};
108+
},
109+
};
110+
111+
function isReadOnlyPropertyUsingAliasOrReads(
112+
node,
113+
readOnlyProperties,
114+
importedComputedName,
115+
importedAliasName,
116+
importedReadsName
117+
) {
118+
// Looks for: reads('readOnlyProperty') or alias('readOnlyProperty')
119+
return (
120+
(isAliasComputedProperty(node, importedComputedName, importedAliasName) ||
121+
isReadsComputedProperty(node, importedComputedName, importedReadsName)) &&
122+
node.arguments.length === 1 &&
123+
isStringLiteral(node.arguments[0]) &&
124+
isReadOnlyProperty(node.arguments[0].value, readOnlyProperties)
125+
);
126+
}
127+
128+
function isThisSet(node) {
129+
// Looks for: this.set('readOnlyProperty...', ...);
130+
return (
131+
node.callee.type === 'MemberExpression' &&
132+
node.callee.object.type === 'ThisExpression' &&
133+
node.callee.property.type === 'Identifier' &&
134+
node.callee.property.name === 'set' &&
135+
node.arguments.length === 2 &&
136+
isStringLiteral(node.arguments[0])
137+
);
138+
}
139+
140+
function isThisSetReadOnlyProperty(node, readOnlyProperties) {
141+
return isThisSet(node) && isReadOnlyProperty(node.arguments[0].value, readOnlyProperties);
142+
}
143+
144+
function isAliasComputedProperty(node, importedComputedName, importedAliasName) {
145+
return (
146+
isIdentifierCall(node, importedAliasName) ||
147+
isMemberExpressionCall(node, importedComputedName, 'alias')
148+
);
149+
}
150+
151+
function isReadsComputedProperty(node, importedComputedName, importedReadsName) {
152+
return (
153+
isIdentifierCall(node, importedReadsName) ||
154+
isMemberExpressionCall(node, importedComputedName, 'reads')
155+
);
156+
}
157+
158+
function isIdentifierCall(node, name) {
159+
return (
160+
node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === name
161+
);
162+
}
163+
164+
function isMemberExpressionCall(node, object, name) {
165+
return (
166+
node.type === 'CallExpression' &&
167+
node.callee.type === 'MemberExpression' &&
168+
node.callee.object.type === 'Identifier' &&
169+
node.callee.object.name === object &&
170+
node.callee.property.type === 'Identifier' &&
171+
node.callee.property.name === name
172+
);
173+
}
174+
175+
function isReadOnlyProperty(property, readOnlyProperties) {
176+
return (
177+
readOnlyProperties.includes(property) ||
178+
readOnlyProperties.some((propertyCurrent) => property.startsWith(`${propertyCurrent}.`))
179+
);
180+
}

0 commit comments

Comments
 (0)