Skip to content

Commit

Permalink
feat: no classes in return type
Browse files Browse the repository at this point in the history
  • Loading branch information
mshanemc committed Mar 6, 2023
1 parent c125192 commit 0227af2
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { noUnnecessaryAliases } from './rules/no-unnecessary-aliases';
import { noMissingMessages } from './rules/no-missing-messages';
import { noArgsParseWithoutStrictFalse } from './rules/no-args-parse-without-strict-false';
import { noHyphenAliases } from './rules/no-hyphens-aliases';
import { noClassesInCommandReturnType } from './rules/no-classes-in-command-return-type';

const library = {
plugins: ['sf-plugin'],
Expand Down Expand Up @@ -76,6 +77,7 @@ const recommended = {
'sf-plugin/no-unnecessary-aliases': 'error',
'sf-plugin/no-args-parse-without-strict-false': 'error',
'sf-plugin/no-hyphens-aliases': 'error',
'sf-plugin/no-classes-in-command-return-type': 'error',
},
};

Expand Down Expand Up @@ -142,5 +144,6 @@ export = {
'no-missing-messages': noMissingMessages,
'no-args-parse-without-strict-false': noArgsParseWithoutStrictFalse,
'no-hyphens-aliases': noHyphenAliases,
'no-classes-in-command-return-type': noClassesInCommandReturnType,
},
};
86 changes: 86 additions & 0 deletions src/rules/no-classes-in-command-return-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { AST_NODE_TYPES, ESLintUtils, ParserServices } from '@typescript-eslint/utils';
import * as ts from 'typescript';
import { isRunMethod } from '../shared/commands';

export const noClassesInCommandReturnType = ESLintUtils.RuleCreator.withoutDocs({
meta: {
docs: {
description: 'The return type of the run method should not contain a class.',
recommended: 'error',
},
messages: {
summary:
'The return type of the run method should not contain a class. Return something that can be expressed as an object so that it supports JSON.',
},
type: 'problem',
schema: [],
fixable: 'code',
},
defaultOptions: [],
create(context) {
return {
// eslint-disable-next-line complexity
MethodDefinition(node): void {
if (
isRunMethod(node) &&
node.value.returnType?.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference &&
node.value.returnType?.typeAnnotation.typeParameters.params[0].type === AST_NODE_TYPES.TSTypeReference
) {
const parserServices = ESLintUtils.getParserServices(context);
const runType = node.value.returnType?.typeAnnotation.typeParameters.params[0];

const realNode = parserServices.esTreeNodeToTSNodeMap.get(runType);

const usesClass = hasOrIsClass(realNode, parserServices);
if (usesClass) {
return context.report({
node: node.value.returnType?.typeAnnotation.typeParameters.params[0],
messageId: 'summary',
});
}
}
},
};
},
});

const hasOrIsClass = (tn: ts.TypeNode | ts.TypeElement, parserServices: ParserServices): boolean => {
// get the TS for this node
const checker = parserServices.program.getTypeChecker();
// follow the type to where it came from
const underlyingNode = checker.getSymbolAtLocation(tn.getChildAt(0));
const declaration = underlyingNode?.getDeclarations()?.[0];
if (!declaration) return false;
if (ts.isClassLike(declaration)) {
return true;
}
if (ts.isInterfaceDeclaration(declaration) || ts.isTypeLiteralNode(declaration)) {
return declaration.members.some((m) => hasOrIsClass(m, parserServices));
}
if (ts.isTypeAliasDeclaration(declaration) && ts.isTypeLiteralNode(declaration.type)) {
return declaration.type.members.some((m) => hasOrIsClass(m, parserServices));
}
if (
(ts.isPropertyDeclaration(declaration) || ts.isPropertySignature(declaration)) &&
ts.isTypeNode(declaration.type)
) {
return hasOrIsClass(declaration.type, parserServices);
}
if (ts.isImportSpecifier(declaration)) {
// Follow the import
const type = checker.getTypeAtLocation(declaration);
const symbolDeclarations = type.getSymbol().getDeclarations();
return symbolDeclarations.some(
(d) => ts.isClassLike(d) || ((ts.isTypeNode(d) || ts.isTypeElement(d)) && hasOrIsClass(d, parserServices))
);
}
// anything other than a type/interface/class

return false;
};
113 changes: 113 additions & 0 deletions test/rules/no-classes-in-command-return-type.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import path from 'path';
import { ESLintUtils } from '@typescript-eslint/utils';
import { noClassesInCommandReturnType } from '../../src/rules/no-classes-in-command-return-type';

const ruleTester = new ESLintUtils.RuleTester({
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: path.join(path.join(__dirname, '..')),
},
});

ruleTester.run('noClassesInCommandReturnType', noClassesInCommandReturnType, {
valid: [
{
name: 'return a type',
code: `
export type FooReturn = { foo: string }
export default class EnvCreateScratch extends SfCommand<FooReturn> {
public async run(): Promise<FooReturn> {
return {foo: 'bar'}
}
}
`,
},
{
name: 'return an interface',
code: `
export interface FooReturn { foo: string }
export default class EnvCreateScratch extends SfCommand<FooReturn> {
public async run(): Promise<FooReturn> {
return {foo: 'bar'}
}
}
`,
},
],
invalid: [
{
name: 'return a class',
errors: [{ messageId: 'summary' }],
code: `
export class FooReturn {}
export default class EnvCreateScratch extends SfCommand<FooReturn> {
public async run(): Promise<FooReturn> {
return new Foo();
}
}
`,
output: null,
},
{
name: 'return a class inside an interface',
errors: [{ messageId: 'summary' }],
code: `
export class FooClass {}
export interface FooReturn {
foo: FooClass
}
export default class EnvCreateScratch extends SfCommand<FooReturn> {
public async run(): Promise<FooReturn> {
return {
foo: new FooClass()
};
}
}
`,
output: null,
},
{
name: 'return a class inside an type',
errors: [{ messageId: 'summary' }],
code: `
export class FooClass {}
export type FooReturn = {
foo: FooClass
}
export default class EnvCreateScratch extends SfCommand<FooReturn> {
public async run(): Promise<FooReturn> {
return {
foo: new FooClass()
};
}
}
`,
output: null,
},
{
name: 'return an imported class',
errors: [{ messageId: 'summary' }],
code: `
import { Messages } from '@salesforce/core';
export type FooReturn = {
foo: Messages
}
export default class EnvCreateScratch extends SfCommand<FooReturn> {
public async run(): Promise<FooReturn> {
return {
foo: new Messages()
};
}
}
`,
output: null,
},
],
});

0 comments on commit 0227af2

Please sign in to comment.