Skip to content

Commit 391a72a

Browse files
Add native class support for model ordering rules
1 parent 538ed00 commit 391a72a

9 files changed

Lines changed: 436 additions & 21 deletions

File tree

docs/rules/no-empty-attrs.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,3 @@ export default Model.extend({
3535
```
3636

3737
In case you need a custom behavior, it's good to write your own [transform](https://api.emberjs.com/ember-data/release/classes/Transform).
38-
39-
## Help Wanted
40-
41-
| Issue | Link |
42-
| :----------------------------------------- | :------------------------------------------------------------------ |
43-
| ❌ Missing native JavaScript class support | [#560](https://github.com/ember-cli/eslint-plugin-ember/issues/560) |

docs/rules/order-in-models.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ You should write code grouped and ordered in this way:
5151
4. Multiline computed properties
5252
5. Other structures (custom methods etc.)
5353

54+
This rule checks ordering only; it does not enforce indentation or other whitespace formatting.
55+
5456
## Examples
5557

5658
```js
@@ -85,9 +87,3 @@ export default Model.extend({
8587
shape: attr('string'),
8688
});
8789
```
88-
89-
## Help Wanted
90-
91-
| Issue | Link |
92-
| :----------------------------------------- | :------------------------------------------------------------------ |
93-
| ❌ Missing native JavaScript class support | [#560](https://github.com/ember-cli/eslint-plugin-ember/issues/560) |

lib/rules/no-empty-attrs.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const ember = require('../utils/ember');
4+
const decoratorUtils = require('../utils/decorators');
45

56
//------------------------------------------------------------------------------
67
// Ember Data - Be explicit with Ember data attribute types
@@ -31,6 +32,24 @@ module.exports = {
3132
const sourceCode = context.sourceCode;
3233
const { scopeManager } = sourceCode;
3334

35+
function reportNativeClassAttrs(node) {
36+
for (const property of node.body.body) {
37+
const attrDecorator = decoratorUtils.findDecorator(property, 'attr');
38+
39+
if (!attrDecorator) {
40+
continue;
41+
}
42+
43+
if (
44+
attrDecorator.expression.type === 'Identifier' ||
45+
(attrDecorator.expression.type === 'CallExpression' &&
46+
attrDecorator.expression.arguments.length === 0)
47+
) {
48+
report(attrDecorator.expression);
49+
}
50+
}
51+
}
52+
3453
return {
3554
CallExpression(node) {
3655
if (!ember.isDSModel(node, filePath)) {
@@ -48,6 +67,20 @@ module.exports = {
4867
}
4968
}
5069
},
70+
ClassDeclaration(node) {
71+
if (!ember.isEmberDataModel(context, node)) {
72+
return;
73+
}
74+
75+
reportNativeClassAttrs(node);
76+
},
77+
ClassExpression(node) {
78+
if (!ember.isEmberDataModel(context, node)) {
79+
return;
80+
}
81+
82+
reportNativeClassAttrs(node);
83+
},
5184
};
5285
},
5386
};

lib/rules/order-in-models.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,40 @@ module.exports = {
8686
return;
8787
}
8888

89+
reportUnorderedProperties(
90+
node,
91+
context,
92+
'model',
93+
order,
94+
importedEmberName,
95+
importedInjectName,
96+
importedObserverName,
97+
importedControllerName,
98+
scopeManager
99+
);
100+
},
101+
ClassDeclaration(node) {
102+
if (!ember.isEmberDataModel(context, node)) {
103+
return;
104+
}
105+
106+
reportUnorderedProperties(
107+
node,
108+
context,
109+
'model',
110+
order,
111+
importedEmberName,
112+
importedInjectName,
113+
importedObserverName,
114+
importedControllerName,
115+
scopeManager
116+
);
117+
},
118+
ClassExpression(node) {
119+
if (!ember.isEmberDataModel(context, node)) {
120+
return;
121+
}
122+
89123
reportUnorderedProperties(
90124
node,
91125
context,

lib/utils/ember.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const kebabCase = require('lodash.kebabcase');
1111

1212
module.exports = {
1313
isDSModel,
14+
isEmberDataModel,
1415
isModule,
1516
isModuleByFilePath,
1617
isMirageDirectory,
@@ -158,6 +159,55 @@ function isDSModel(node, filePath) {
158159
return isModule(node, 'Model', 'DS') || isModuleByPath;
159160
}
160161

162+
function isEmberDataModel(context, node) {
163+
if (!(types.isClassDeclaration(node) || node.type === 'ClassExpression')) {
164+
assert(
165+
false,
166+
'Function should only be called on a `ClassDeclaration`/`ClassExpression` (native class)'
167+
);
168+
}
169+
170+
if (!node.superClass) {
171+
return false;
172+
}
173+
174+
if (types.isIdentifier(node.superClass)) {
175+
const superClassImportPath = importUtils.getSourceModuleNameForIdentifier(
176+
context,
177+
node.superClass
178+
);
179+
180+
if (
181+
superClassImportPath === '@ember-data/model' ||
182+
superClassImportPath === 'ember-data/model'
183+
) {
184+
return true;
185+
}
186+
}
187+
188+
if (
189+
types.isCallExpression(node.superClass) &&
190+
types.isMemberExpression(node.superClass.callee) &&
191+
types.isIdentifier(node.superClass.callee.property) &&
192+
node.superClass.callee.property.name === 'extend' &&
193+
types.isIdentifier(node.superClass.callee.object)
194+
) {
195+
const superClassImportPath = importUtils.getSourceModuleNameForIdentifier(
196+
context,
197+
node.superClass.callee.object
198+
);
199+
200+
if (
201+
superClassImportPath === '@ember-data/model' ||
202+
superClassImportPath === 'ember-data/model'
203+
) {
204+
return true;
205+
}
206+
}
207+
208+
return isModuleByFilePath(context.filename, 'model');
209+
}
210+
161211
function isModuleByFilePath(filePath, module) {
162212
const expectedFileNameJs = `${module}.js`;
163213
const expectedFileNameTs = `${module}.ts`;

lib/utils/property-order.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,15 @@ function reportUnorderedProperties(
208208
importedControllerName,
209209
scopeManager
210210
) {
211+
const isNativeClass = types.isClassDeclaration(node) || node.type === 'ClassExpression';
211212
let maxOrder = -1;
212213
const firstPropertyOfType = {};
213214
let firstPropertyOfNextType;
214215

215-
const properties = types.isClassDeclaration(node)
216-
? node.body.body
217-
: ember.getModuleProperties(node, scopeManager);
216+
const properties =
217+
types.isClassDeclaration(node) || node.type === 'ClassExpression'
218+
? node.body.body
219+
: ember.getModuleProperties(node, scopeManager);
218220

219221
for (const property of properties) {
220222
const type = determinePropertyType(
@@ -270,18 +272,18 @@ function reportUnorderedProperties(
270272
nextToken = previousToken;
271273
}
272274

273-
// adding a trailing comma when it's the last property defined
275+
// adding a trailing comma when it's the last property defined in object literals
274276
if (nextToken.value === '}') {
275277
isLastProperty = true;
276278

277-
if (previousToken.value !== ',') {
279+
if (!isNativeClass && previousToken.value !== ',') {
278280
optionalComma = ',';
279281
}
280282
}
281283
} while (nextToken.value === ',');
282284

283-
// additional whitespace is needed only when it's the last property
284-
const whitespaceCount = isLastProperty ? property.loc.start.column : 0;
285+
// additional whitespace is needed only when it's the last property in object literals
286+
const whitespaceCount = !isNativeClass && isLastProperty ? property.loc.start.column : 0;
285287

286288
const propertyOffset = getCommentOffsetBefore(property, sourceCode);
287289
const foundPropertyOffset = getCommentOffsetBefore(foundProperty, sourceCode);

tests/lib/rules/no-empty-attrs.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ const RuleTester = require('eslint').RuleTester;
1010
// ------------------------------------------------------------------------------
1111

1212
const eslintTester = new RuleTester({
13-
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
13+
parser: require.resolve('@babel/eslint-parser'),
14+
parserOptions: {
15+
ecmaVersion: 2022,
16+
sourceType: 'module',
17+
babelOptions: {
18+
configFile: require.resolve('../../../.babelrc'),
19+
},
20+
},
1421
});
1522

1623
const message = 'Supply proper attribute type';
@@ -31,6 +38,10 @@ eslintTester.run('no-empty-attrs', rule, {
3138
return attr.underscore();
3239
}),
3340
});`,
41+
`import Model, { attr } from '@ember-data/model';
42+
export default class UserModel extends Model {
43+
@attr('string') name;
44+
}`,
3445
],
3546
invalid: [
3647
{
@@ -98,5 +109,29 @@ eslintTester.run('no-empty-attrs', rule, {
98109
output: null,
99110
errors: [{ message, line: 1 }],
100111
},
112+
{
113+
code: `import Model, { attr } from '@ember-data/model';
114+
export default class UserModel extends Model {
115+
@attr() name;
116+
}`,
117+
output: null,
118+
errors: [{ message, line: 3 }],
119+
},
120+
{
121+
code: `import Model, { attr } from '@ember-data/model';
122+
export default class UserModel extends Model {
123+
@attr name;
124+
}`,
125+
output: null,
126+
errors: [{ message, line: 3 }],
127+
},
128+
{
129+
code: `import Model, { attr } from '@ember-data/model';
130+
export default (class UserModel extends Model {
131+
@attr name;
132+
});`,
133+
output: null,
134+
errors: [{ message, line: 3 }],
135+
},
101136
],
102137
});

0 commit comments

Comments
 (0)