Skip to content

Commit

Permalink
Add new rule no-deprecated-router-transition-methods (#1715)
Browse files Browse the repository at this point in the history
closes #1074
  • Loading branch information
rtablada authored Dec 30, 2022
1 parent 6cc7860 commit e965de5
Show file tree
Hide file tree
Showing 5 changed files with 1,129 additions and 12 deletions.
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,18 +96,19 @@ module.exports = {

### Deprecations

| Name | Description | 💼 | 🔧 | 💡 |
| :--------------------------------------------------------------------------------- | :-------------------------------------------------------- | :- | :- | :- |
| [closure-actions](docs/rules/closure-actions.md) | enforce usage of closure actions || | |
| [new-module-imports](docs/rules/new-module-imports.md) | enforce using "New Module Imports" from Ember RFC #176 || | |
| [no-array-prototype-extensions](docs/rules/no-array-prototype-extensions.md) | disallow usage of Ember's `Array` prototype extensions | | 🔧 | |
| [no-function-prototype-extensions](docs/rules/no-function-prototype-extensions.md) | disallow usage of Ember's `function` prototype extensions || | |
| [no-implicit-injections](docs/rules/no-implicit-injections.md) | enforce usage of implicit service injections | | 🔧 | |
| [no-mixins](docs/rules/no-mixins.md) | disallow the usage of mixins || | |
| [no-new-mixins](docs/rules/no-new-mixins.md) | disallow the creation of new mixins || | |
| [no-observers](docs/rules/no-observers.md) | disallow usage of observers || | |
| [no-old-shims](docs/rules/no-old-shims.md) | disallow usage of old shims for modules || 🔧 | |
| [no-string-prototype-extensions](docs/rules/no-string-prototype-extensions.md) | disallow usage of `String` prototype extensions || | |
| Name | Description | 💼 | 🔧 | 💡 |
| :----------------------------------------------------------------------------------------------- | :-------------------------------------------------------- | :- | :- | :- |
| [closure-actions](docs/rules/closure-actions.md) | enforce usage of closure actions || | |
| [new-module-imports](docs/rules/new-module-imports.md) | enforce using "New Module Imports" from Ember RFC #176 || | |
| [no-array-prototype-extensions](docs/rules/no-array-prototype-extensions.md) | disallow usage of Ember's `Array` prototype extensions | | 🔧 | |
| [no-deprecated-router-transition-methods](docs/rules/no-deprecated-router-transition-methods.md) | enforce usage of router service transition methods | | 🔧 | |
| [no-function-prototype-extensions](docs/rules/no-function-prototype-extensions.md) | disallow usage of Ember's `function` prototype extensions || | |
| [no-implicit-injections](docs/rules/no-implicit-injections.md) | enforce usage of implicit service injections | | 🔧 | |
| [no-mixins](docs/rules/no-mixins.md) | disallow the usage of mixins || | |
| [no-new-mixins](docs/rules/no-new-mixins.md) | disallow the creation of new mixins || | |
| [no-observers](docs/rules/no-observers.md) | disallow usage of observers || | |
| [no-old-shims](docs/rules/no-old-shims.md) | disallow usage of old shims for modules || 🔧 | |
| [no-string-prototype-extensions](docs/rules/no-string-prototype-extensions.md) | disallow usage of `String` prototype extensions || | |

### Ember Data

Expand Down
92 changes: 92 additions & 0 deletions docs/rules/no-deprecated-router-transition-methods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# ember/no-deprecated-router-transition-methods

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

Ember 3.26 introduced a deprecation for using `transitionTo` and `replaceWith` in Routes or `transitionToRoute` and `replaceRoute` in Controllers. These methods should be replaced with an injected router service and calls to `this.router.transitionTo` and `this.router.replaceWith` instead.

## Rule Details

This rule checks for uses of `transitionTo` and `replaceWith` in Routes or `transitionToRoute` and `replaceRoute` in Controllers.

## Examples

Examples of **incorrect** code for this rule:

```js
// app/routes/settings.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class SettingsRoute extends Route {
@service session;

beforeModel() {
if (!this.session.isAuthenticated) {
this.transitionTo('login');
}
}
}
```

```js
// app/controllers/new-post.js
import Controller from '@ember/controller';
import { action } from '@ember/object';

export default class NewPostController extends Controller {
@action
async save({ title, text }) {
const post = this.store.createRecord('post', { title, text });
await post.save();
return this.transitionToRoute('post', post.id);
}
}
```

Examples of **correct** code for this rule:

```js
// app/routes/settings.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default class SettingsRoute extends Route {
@service('router') router;
@service('session') session;

beforeModel() {
if (!this.session.isAuthenticated) {
this.router.transitionTo('login');
}
}
}
```

```js
// app/controllers/new-post.js
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';

export default class NewPostController extends Controller {
@service('router') router;

@action
async save({ title, text }) {
const post = this.store.createRecord('post', { title, text });
await post.save();
return this.router.transitionTo('post', post.id);
}
}
```

## Migration

The autofixer for this rule will update method calls to use the router service, and will inject the router service as needed.

## References

- [Deprecation](https://deprecations.emberjs.com/v3.x/#toc_routing-transition-methods)
- [Router Service](https://api.emberjs.com/ember/release/classes/RouterService)
245 changes: 245 additions & 0 deletions lib/rules/no-deprecated-router-transition-methods.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
'use strict';

const emberUtils = require('../utils/ember');
const { getImportIdentifier } = require('../utils/import');
const Stack = require('../utils/stack');
const types = require('../utils/types');
const decoratorUtils = require('../utils/decorators');

function getBaseFixSteps(fixer, context, currentClass) {
const fixSteps = [];
const sourceCode = context.getSourceCode();
let serviceInjectImportName = currentClass.serviceInjectImportName;
let routerServicePropertyName = currentClass.routerServicePropertyName;

if (!serviceInjectImportName) {
fixSteps.push(
fixer.insertTextBefore(
sourceCode.ast,
"import { inject as service } from '@ember/service';\n"
)
);

serviceInjectImportName = 'service';
}

if (!routerServicePropertyName) {
fixSteps.push(
currentClass.node.type === 'CallExpression'
? fixer.insertTextBefore(
currentClass.node.arguments[0].properties[0],
`router: ${serviceInjectImportName}('router'),\n`
)
: fixer.insertTextBefore(
currentClass.node.body.body[0],
`@${serviceInjectImportName}('router') router;\n`
)
);

routerServicePropertyName = 'router';
}

return { fixSteps, routerServicePropertyName };
}

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce usage of router service transition methods',
category: 'Deprecations',
recommended: false,
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-deprecated-router-transition-methods.md',
},
fixable: 'code',
schema: [],
messages: {
main: 'Calling "{{methodUsed}}" in {{moduleType}}s is deprecated, call {{desiredMethod}} on the injected router service instead.',
},
},

create(context) {
// State being tracked for this file.
let serviceInjectImportName = undefined;
let routerServicePropertyName = undefined;
let isValidModule = false;

// State being tracked for the current class we're inside.
const classStack = new Stack();

function onClassEnter(node) {
if (emberUtils.isAnyEmberCoreModule(context, node)) {
const classMembers = node.body.body;

for (const classMember of classMembers) {
if (emberUtils.isInjectedServiceProp(classMember, undefined, serviceInjectImportName)) {
const serviceExpression = decoratorUtils.findDecorator(
classMember,
serviceInjectImportName
).expression;

if (serviceExpression.type === 'CallExpression') {
if (
(serviceExpression.arguments.length === 0 && classMember.key.name === 'router') ||
(serviceExpression.arguments.length > 0 &&
serviceExpression.arguments[0].value === 'router')
) {
routerServicePropertyName = classMember.key.name;
}
} else if (classMember.key.name === 'router') {
routerServicePropertyName = classMember.key.name;
}
}
}
const isRoute = emberUtils.isEmberRoute(context, node);
const isController = emberUtils.isEmberController(context, node);
isValidModule = isRoute || isController;

classStack.push({
node,
serviceInjectImportName,
routerServicePropertyName,
isValidModule,
isRoute,
isController,
});
} else {
classStack.push({
node,
isValidModule: false,
});
}
}

function onClassExit(node) {
// Leaving current (native) class.
if (classStack.size() > 0 && classStack.peek().node === node) {
classStack.pop();
}
}

return {
ImportDeclaration(node) {
if (node.source.value === '@ember/service') {
serviceInjectImportName =
serviceInjectImportName || getImportIdentifier(node, '@ember/service', 'inject');
}
},

ClassDeclaration: onClassEnter,
ClassExpression: onClassEnter,
CallExpression(node) {
if (emberUtils.isAnyEmberCoreModule(context, node)) {
const classMembers = node.arguments[0].properties;

for (const classMember of classMembers) {
if (emberUtils.isInjectedServiceProp(classMember, undefined, serviceInjectImportName)) {
const callExpression = classMember.value;

if (
(callExpression.arguments.length === 0 && classMember.key.name === 'router') ||
(callExpression.arguments.length > 0 &&
callExpression.arguments[0].value === 'router')
) {
routerServicePropertyName = classMember.key.name;
}
}
}
const isRoute = emberUtils.isEmberRoute(context, node);
const isController = emberUtils.isEmberController(context, node);
isValidModule = isRoute || isController;

classStack.push({
node,
serviceInjectImportName,
routerServicePropertyName,
isValidModule,
isRoute,
isController,
});
}
},

'ClassDeclaration:exit': onClassExit,
'ClassExpression:exit': onClassExit,
'CallExpression:exit': onClassExit,

MemberExpression(node) {
if (!isValidModule) {
return;
}

const currentClass = classStack.peek();

if (types.isThisExpression(node.object) && types.isIdentifier(node.property)) {
// Routes should not call transitionTo or replaceWith
const propertyName = node.property.name;

if (
currentClass.isRoute &&
(propertyName === 'transitionTo' || propertyName === 'replaceWith')
) {
context.report({
node,
messageId: 'main',
data: {
methodUsed: propertyName,
desiredMethod: propertyName,
moduleType: 'Route',
},
fix(fixer) {
const { routerServicePropertyName, fixSteps } = getBaseFixSteps(
fixer,
context,
currentClass
);

return [
...fixSteps,
fixer.insertTextBefore(node.property, `${routerServicePropertyName}.`),
];
},
});
}

if (
currentClass.isController &&
(propertyName === 'transitionToRoute' || propertyName === 'replaceRoute')
) {
const replacementPropertyName =
propertyName === 'transitionToRoute' ? 'transitionTo' : 'replaceWith';
context.report({
node,
messageId: 'main',
data: {
methodUsed: propertyName,
desiredMethod: replacementPropertyName,
moduleType: 'Controller',
},
fix(fixer) {
const { routerServicePropertyName, fixSteps } = getBaseFixSteps(
fixer,
context,
currentClass
);

return [
...fixSteps,
fixer.replaceText(
node.property,
`${routerServicePropertyName}.${replacementPropertyName}`
),
];
},
});
}
}
},
};
},
};
Loading

0 comments on commit e965de5

Please sign in to comment.