Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): Add a new rule prefer-promise-like to help migrate off of ng.IPromise #8673

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint-plugin/base.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
'@spinnaker/ng-no-require-angularjs': 2,
'@spinnaker/ng-no-require-module-deps': 2,
'@spinnaker/ng-strictdi': 2,
'@spinnaker/prefer-promise-like': 1,
'@spinnaker/react2angular-with-error-boundary.spec.js': 2,
indent: 'off',
'member-ordering': 'off',
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/eslint-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module.exports = {
'ng-no-require-angularjs': require('./rules/ng-no-require-angularjs'),
'ng-no-require-module-deps': require('./rules/ng-no-require-module-deps'),
'ng-strictdi': require('./rules/ng-strictdi'),
'prefer-promise-like': require('./rules/prefer-promise-like'),
'react2angular-with-error-boundary.spec.js': require('./rules/react2angular-with-error-boundary'),
},
configs: {
Expand Down
103 changes: 103 additions & 0 deletions packages/eslint-plugin/rules/prefer-promise-like.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
'use strict';
const _ = require('lodash');

/**
* No slashes in string literals passed to API.one() / API.all()
*
* @version 0.1.0
* @category
*/
const rule = function (context) {
return {
TSTypeReference: function (node) {
// var foo: IPromise<any> = bar()
// ^^^^^^^^
const type_IPromise = {
type: 'TSTypeReference',
typeName: {
type: 'Identifier',
name: 'IPromise',
},
};

// var foo: ng.IPromise<any> = bar()
// ^^^^^^^^^^^
const type_ng_IPromise = {
type: 'TSTypeReference',
typeName: {
type: 'TSQualifiedName',
left: {
type: 'Identifier',
name: 'ng',
},
right: {
type: 'Identifier',
name: 'IPromise',
},
},
};

const message = `Prefer using PromiseLike type instead of AngularJS IPromise.`;
const fix = (fixer) => fixer.replaceText(node.typeName, 'PromiseLike');
if (_.isMatch(node, type_IPromise)) {
context.report({ fix, node: node.typeName, message });
} else if (_.isMatch(node, type_ng_IPromise)) {
context.report({ fix, node: node.typeName, message });
}
},

// If there are any unused IPromise imports, remove them
ImportDeclaration: function (node) {
const importIPromise = {
type: 'ImportSpecifier',
imported: {
type: 'Identifier',
name: 'IPromise',
},
};

const message = `Unused IPromise import`;

// import { foo, IPromise, bar } from 'angular';
// ^^^^^^^^
const specifiers = node.specifiers || [];
const foundIPromiseImport = specifiers.find((s) => _.isMatch(s, importIPromise));

const variables = context.getScope().variables;
const variable = variables.find((x) => x.defs.some((def) => def.node === foundIPromiseImport));
const unused = variable && variable.references.length === 0;

const fix = (fixer) => {
const importCount = node.specifiers.length;
if (importCount === 1) {
// Delete the whole import
return fixer.replaceText(node, '');
} else {
// Delete only IPromise from the import
const source = context
.getSourceCode()
.getText(node)
.replace(/,\s*IPromise/g, '')
.replace(/IPromise\s*,\s*/g, '');

return fixer.replaceText(node, source);
}
};

if (foundIPromiseImport && unused) {
context.report({ node, message, fix });
}
},
};
};

module.exports = {
meta: {
type: 'problem',
docs: {
description: ``,
},
fixable: 'code',
},
create: rule,
};
77 changes: 77 additions & 0 deletions packages/eslint-plugin/test/prefer-promise-like.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
'use strict';

const ruleTester = require('../utils/ruleTester');
const rule = require('../rules/prefer-promise-like');
const errorMessage = `Prefer using PromiseLike type instead of AngularJS IPromise.`;
const unusedImportErrorMessage = `Unused IPromise import`;

ruleTester.run('prefer-promise-like', rule, {
valid: [
{
code: `const foo: PromiseLike<any> = API.one('foo', 'bar').get();`,
},
],

invalid: [
// IPromise in variable
{
code: `const foo: IPromise<any> = API.one('foo', 'bar').get();`,
output: `const foo: PromiseLike<any> = API.one('foo', 'bar').get();`,
errors: [errorMessage],
},
// IPromise in function arg
{
code: `function foo(promise: IPromise<any>) {}`,
output: `function foo(promise: PromiseLike<any>) {}`,
errors: [errorMessage],
},
// IPromise in class method return
{
code: `class Foo { foo(): IPromise<any> {} }`,
output: `class Foo { foo(): PromiseLike<any> {} }`,
errors: [errorMessage],
},
// ng.IPromise in variable
{
code: `const foo: ng.IPromise<any> = API.one('foo', 'bar').get();`,
output: `const foo: PromiseLike<any> = API.one('foo', 'bar').get();`,
errors: [errorMessage],
},
// ng.IPromise in function arg
{
code: `function foo(promise: ng.IPromise<any>) {}`,
output: `function foo(promise: PromiseLike<any>) {}`,
errors: [errorMessage],
},
// ng.IPromise in class method return
{
code: `class Foo { foo(): ng.IPromise<any> {} }`,
output: `class Foo { foo(): PromiseLike<any> {} }`,
errors: [errorMessage],
},
// Unused IPromise import
{
code: `import { IPromise } from 'angular';`,
output: ``,
errors: [unusedImportErrorMessage],
},
// Unused IPromise import 2
{
code: `import { module, IPromise } from 'angular';`,
output: `import { module } from 'angular';`,
errors: [unusedImportErrorMessage],
},
// Unused IPromise import 3
{
code: `import { IPromise, module } from 'angular';`,
output: `import { module } from 'angular';`,
errors: [unusedImportErrorMessage],
},
// Unused IPromise import 4
{
code: `import { QService, IPromise, module } from 'angular';`,
output: `import { QService, module } from 'angular';`,
errors: [unusedImportErrorMessage],
},
],
});