Skip to content

Commit af62351

Browse files
authored
Add new ember-data rule: require-async-inverse-relationship (#2155)
1 parent 8f485b2 commit af62351

File tree

4 files changed

+330
-4
lines changed

4 files changed

+330
-4
lines changed

README.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,11 @@ rules in templates can be disabled with eslint directives with mustache or html
237237

238238
### Ember Data
239239

240-
| Name                           | Description | 💼 | 🔧 | 💡 |
241-
| :----------------------------------------------------------------------------- | :------------------------------------------------------------------- | :- | :- | :- |
242-
| [no-empty-attrs](docs/rules/no-empty-attrs.md) | disallow usage of empty attributes in Ember Data models | | | |
243-
| [use-ember-data-rfc-395-imports](docs/rules/use-ember-data-rfc-395-imports.md) | enforce usage of `@ember-data/` package imports instead `ember-data` || 🔧 | |
240+
| Name                               | Description | 💼 | 🔧 | 💡 |
241+
| :------------------------------------------------------------------------------------- | :-------------------------------------------------------------------- | :- | :- | :- |
242+
| [no-empty-attrs](docs/rules/no-empty-attrs.md) | disallow usage of empty attributes in Ember Data models | | | |
243+
| [require-async-inverse-relationship](docs/rules/require-async-inverse-relationship.md) | require inverse to be specified in @belongsTo and @hasMany decorators | | | |
244+
| [use-ember-data-rfc-395-imports](docs/rules/use-ember-data-rfc-395-imports.md) | enforce usage of `@ember-data/` package imports instead `ember-data` || 🔧 | |
244245

245246
### Ember Object
246247

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# ember/require-async-inverse-relationship
2+
3+
<!-- end auto-generated rule header -->
4+
5+
This rule ensures that the `async` and `inverse` properties are specified in `@belongsTo` and `@hasMany` decorators in Ember Data models.
6+
7+
## Rule Details
8+
9+
This rule disallows:
10+
11+
- Using `@belongsTo` without specifying the `async` and `inverse` properties.
12+
- Using `@hasMany` without specifying the `async` and `inverse` properties.
13+
14+
## Examples
15+
16+
Examples of **incorrect** code for this rule:
17+
18+
```js
19+
import Model, { belongsTo, hasMany } from '@ember-data/model';
20+
21+
export default class FolderModel extends Model {
22+
@hasMany('folder', { inverse: 'parent' }) children;
23+
@belongsTo('folder', { inverse: 'children' }) parent;
24+
}
25+
```
26+
27+
Examples of **correct** code for this rule:
28+
29+
```js
30+
import Model, { belongsTo, hasMany } from '@ember-data/model';
31+
32+
export default class FolderModel extends Model {
33+
@hasMany('folder', { async: true, inverse: 'parent' }) children;
34+
@belongsTo('folder', { async: true, inverse: 'children' }) parent;
35+
}
36+
```
37+
38+
## References
39+
40+
- [Deprecate Non Strict Relationships](https://deprecations.emberjs.com/id/ember-data-deprecate-non-strict-relationships)
41+
- [Ember Data Relationships](https://guides.emberjs.com/release/models/relationships)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'suggestion',
5+
docs: {
6+
description: 'require inverse to be specified in @belongsTo and @hasMany decorators',
7+
category: 'Ember Data',
8+
recommended: false,
9+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/require-async-inverse-relationship.md',
10+
},
11+
schema: [],
12+
},
13+
14+
create(context) {
15+
return {
16+
CallExpression(node) {
17+
const decorator =
18+
node.parent.type === 'Decorator' &&
19+
['belongsTo', 'hasMany'].includes(node.callee.name) &&
20+
node;
21+
22+
if (decorator) {
23+
const args = decorator.arguments;
24+
const hasAsync = args.some(
25+
(arg) =>
26+
arg.type === 'ObjectExpression' &&
27+
arg.properties.some((prop) => prop.key.name === 'async')
28+
);
29+
const hasBooleanAsync = args.some(
30+
(arg) =>
31+
arg.type === 'ObjectExpression' &&
32+
arg.properties.some(
33+
(prop) => prop.key.name === 'async' && typeof prop.value.value === 'boolean'
34+
)
35+
);
36+
const hasInverse = args.some(
37+
(arg) =>
38+
arg.type === 'ObjectExpression' &&
39+
arg.properties.some((prop) => prop.key.name === 'inverse')
40+
);
41+
42+
if (!hasAsync) {
43+
context.report({
44+
node,
45+
message: 'The @{{decorator}} decorator requires an `async` property to be specified.',
46+
data: {
47+
decorator: decorator.callee.name,
48+
},
49+
});
50+
} else if (!hasBooleanAsync) {
51+
context.report({
52+
node,
53+
message:
54+
'The @{{decorator}} decorator requires an `async` property to be specified as a boolean.',
55+
data: {
56+
decorator: decorator.callee.name,
57+
},
58+
});
59+
}
60+
61+
if (!hasInverse) {
62+
context.report({
63+
node,
64+
message:
65+
'The @{{decorator}} decorator requires an `inverse` property to be specified.',
66+
data: {
67+
decorator: decorator.callee.name,
68+
},
69+
});
70+
}
71+
}
72+
},
73+
};
74+
},
75+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
'use strict';
2+
3+
//------------------------------------------------------------------------------
4+
// Requirements
5+
//------------------------------------------------------------------------------
6+
7+
const rule = require('../../../lib/rules/require-async-inverse-relationship');
8+
const RuleTester = require('eslint').RuleTester;
9+
10+
const parserOptions = { ecmaVersion: 2022, sourceType: 'module' };
11+
12+
const ruleTester = new RuleTester({
13+
parserOptions,
14+
parser: require.resolve('@babel/eslint-parser'),
15+
});
16+
17+
//------------------------------------------------------------------------------
18+
// Tests
19+
//------------------------------------------------------------------------------
20+
21+
ruleTester.run('require-async-inverse-relationship', rule, {
22+
valid: [
23+
`import Model, { belongsTo } from '@ember-data/model';
24+
25+
export default class extends Model {
26+
@belongsTo('post', { async: true, inverse: 'comments' }) post;
27+
}`,
28+
`import Model, { hasMany } from '@ember-data/model';
29+
30+
export default class extends Model {
31+
@hasMany('comment', { async: true, inverse: 'post' }) comments;
32+
}`,
33+
`import Model, { belongsTo, hasMany } from '@ember-data/model';
34+
35+
export default class extends Model {
36+
@belongsTo('post', { async: false, inverse: 'comments' }) post;
37+
@hasMany('user', { async: true, inverse: null }) owner;
38+
}`,
39+
],
40+
41+
invalid: [
42+
{
43+
code: `import Model, { belongsTo } from '@ember-data/model';
44+
45+
export default class extends Model {
46+
@belongsTo('post') post;
47+
}`,
48+
output: null,
49+
errors: [
50+
{
51+
message: 'The @belongsTo decorator requires an `async` property to be specified.',
52+
type: 'CallExpression',
53+
},
54+
{
55+
message: 'The @belongsTo decorator requires an `inverse` property to be specified.',
56+
type: 'CallExpression',
57+
},
58+
],
59+
},
60+
{
61+
code: `import Model, { belongsTo } from '@ember-data/model';
62+
63+
export default class extends Model {
64+
@belongsTo('post', {}) post;
65+
}`,
66+
output: null,
67+
errors: [
68+
{
69+
message: 'The @belongsTo decorator requires an `async` property to be specified.',
70+
type: 'CallExpression',
71+
},
72+
{
73+
message: 'The @belongsTo decorator requires an `inverse` property to be specified.',
74+
type: 'CallExpression',
75+
},
76+
],
77+
},
78+
{
79+
code: `import Model, { belongsTo } from '@ember-data/model';
80+
81+
export default class extends Model {
82+
@belongsTo('post', { async: 'comments'}) post;
83+
}`,
84+
output: null,
85+
errors: [
86+
{
87+
message:
88+
'The @belongsTo decorator requires an `async` property to be specified as a boolean.',
89+
type: 'CallExpression',
90+
},
91+
{
92+
message: 'The @belongsTo decorator requires an `inverse` property to be specified.',
93+
type: 'CallExpression',
94+
},
95+
],
96+
},
97+
{
98+
code: `import Model, { belongsTo } from '@ember-data/model';
99+
100+
export default class extends Model {
101+
@belongsTo('post', { async: true }) post;
102+
}`,
103+
output: null,
104+
errors: [
105+
{
106+
message: 'The @belongsTo decorator requires an `inverse` property to be specified.',
107+
type: 'CallExpression',
108+
},
109+
],
110+
},
111+
{
112+
code: `import Model, { belongsTo } from '@ember-data/model';
113+
114+
export default class extends Model {
115+
@belongsTo('post', { inverse: 'comments' }) post;
116+
}`,
117+
output: null,
118+
errors: [
119+
{
120+
message: 'The @belongsTo decorator requires an `async` property to be specified.',
121+
type: 'CallExpression',
122+
},
123+
],
124+
},
125+
{
126+
code: `import Model, { hasMany } from '@ember-data/model';
127+
128+
export default class extends Model {
129+
@hasMany('comment') comments;
130+
}`,
131+
output: null,
132+
errors: [
133+
{
134+
message: 'The @hasMany decorator requires an `async` property to be specified.',
135+
type: 'CallExpression',
136+
},
137+
{
138+
message: 'The @hasMany decorator requires an `inverse` property to be specified.',
139+
type: 'CallExpression',
140+
},
141+
],
142+
},
143+
{
144+
code: `import Model, { hasMany } from '@ember-data/model';
145+
146+
export default class extends Model {
147+
@hasMany('comment', {}) comments;
148+
}`,
149+
output: null,
150+
errors: [
151+
{
152+
message: 'The @hasMany decorator requires an `async` property to be specified.',
153+
type: 'CallExpression',
154+
},
155+
{
156+
message: 'The @hasMany decorator requires an `inverse` property to be specified.',
157+
type: 'CallExpression',
158+
},
159+
],
160+
},
161+
{
162+
code: `import Model, { hasMany } from '@ember-data/model';
163+
164+
export default class extends Model {
165+
@hasMany('comment', { async: 'comments'}) comments;
166+
}`,
167+
output: null,
168+
errors: [
169+
{
170+
message:
171+
'The @hasMany decorator requires an `async` property to be specified as a boolean.',
172+
type: 'CallExpression',
173+
},
174+
{
175+
message: 'The @hasMany decorator requires an `inverse` property to be specified.',
176+
type: 'CallExpression',
177+
},
178+
],
179+
},
180+
{
181+
code: `import Model, { hasMany } from '@ember-data/model';
182+
183+
export default class extends Model {
184+
@hasMany('comment', { async: true }) comments;
185+
}`,
186+
output: null,
187+
errors: [
188+
{
189+
message: 'The @hasMany decorator requires an `inverse` property to be specified.',
190+
type: 'CallExpression',
191+
},
192+
],
193+
},
194+
{
195+
code: `import Model, { hasMany } from '@ember-data/model';
196+
197+
export default class extends Model {
198+
@hasMany('comment', { inverse: 'post' }) comments;
199+
}`,
200+
output: null,
201+
errors: [
202+
{
203+
message: 'The @hasMany decorator requires an `async` property to be specified.',
204+
type: 'CallExpression',
205+
},
206+
],
207+
},
208+
],
209+
});

0 commit comments

Comments
 (0)