-
-
Notifications
You must be signed in to change notification settings - Fork 204
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add new rule
no-deprecated-router-transition-methods
(#1715)
closes #1074
- Loading branch information
Showing
5 changed files
with
1,129 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}` | ||
), | ||
]; | ||
}, | ||
}); | ||
} | ||
} | ||
}, | ||
}; | ||
}, | ||
}; |
Oops, something went wrong.