Skip to content

Commit

Permalink
feat: only apply in /commands folder
Browse files Browse the repository at this point in the history
  • Loading branch information
mshanemc committed Jun 12, 2022
1 parent 8461761 commit 1c04f52
Show file tree
Hide file tree
Showing 12 changed files with 311 additions and 16 deletions.
9 changes: 9 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { noDuplicateShortCharacters } from './rules/noDuplicateShortCharacters';
import { flagSummary } from './rules/flagSummary';
import { flagCasing } from './rules/flagCasing';
import { extractMessage } from './rules/extractMessage';
import { flagCrossReferences } from './rules/flagCrossReferences';
import { commandSummary } from './rules/commandSummary';
import { commandExamples } from './rules/commandExamples';

export = {
configs: {
recommended: {
plugins: ['sf-plugin'],
rules: {
'sf-plugin/command-summary': 'error',
'sf-plugin/command-example': 'warn',
'sf-plugin/no-duplicate-short-characters': 'error',
'sf-plugin/flag-case': 'error',
'sf-plugin/flag-summary': 'error',
'sf-plugin/no-hardcoded-messages': 'warn',
'sf-plugin/flag-cross-references': 'error',
},
Expand All @@ -24,7 +30,10 @@ export = {
rules: {
'no-duplicate-short-characters': noDuplicateShortCharacters,
'flag-case': flagCasing,
'flag-summary': flagSummary,
'no-hardcoded-messages': extractMessage,
'flag-cross-references': flagCrossReferences,
'command-summary': commandSummary,
'command-example': commandExamples,
},
};
9 changes: 7 additions & 2 deletions src/rules/extractMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
import { ESLintUtils, AST_NODE_TYPES } from '@typescript-eslint/utils';
import { isFlag } from '../shared/flags';
import { ancestorsContainsSfCommand, isInCommandDirectory } from '../shared/commands';

export const extractMessage = ESLintUtils.RuleCreator.withoutDocs({
meta: {
Expand All @@ -24,11 +25,15 @@ export const extractMessage = ESLintUtils.RuleCreator.withoutDocs({
create(context) {
return {
Property(node): void {
if (!isInCommandDirectory(context)) {
return;
}
const ancestors = context.getAncestors();
if (
node.type === AST_NODE_TYPES.Property &&
node.key.type === AST_NODE_TYPES.Identifier &&
(node.key.name === 'summary' || node.key.name === 'description') &&
context.getAncestors().some((a) => isFlag(a))
ancestors.some((a) => isFlag(a)) &&
ancestorsContainsSfCommand(ancestors)
) {
if (node.value.type === 'Literal') {
context.report({
Expand Down
3 changes: 2 additions & 1 deletion src/rules/flagCasing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { ESLintUtils } from '@typescript-eslint/utils';
import { ancestorsContainsSfCommand, isInCommandDirectory } from '../shared/commands';
import { getFlagName, isFlag } from '../shared/flags';

export const flagCasing = ESLintUtils.RuleCreator.withoutDocs({
Expand All @@ -23,7 +24,7 @@ export const flagCasing = ESLintUtils.RuleCreator.withoutDocs({
create(context) {
return {
Property(node): void {
if (isFlag(node)) {
if (isInCommandDirectory(context) && isFlag(node) && ancestorsContainsSfCommand(context.getAncestors())) {
const flagName = getFlagName(node);
if (flagName.toLowerCase() !== flagName) {
context.report({
Expand Down
10 changes: 8 additions & 2 deletions src/rules/flagCrossReferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { ESLintUtils } from '@typescript-eslint/utils';
import { ancestorsContainsSfCommand, isInCommandDirectory } from '../shared/commands';
import { isFlag, isFlagsStaticProperty } from '../shared/flags';

// properties that reference other flags by name
Expand All @@ -26,14 +27,19 @@ export const flagCrossReferences = ESLintUtils.RuleCreator.withoutDocs({
create(context) {
return {
Property(node): void {
if (!isInCommandDirectory(context)) {
return;
}
const ancestors = context.getAncestors();
if (
node.key.type === 'Identifier' &&
node.value.type === 'ArrayExpression' &&
node.value.elements.every((e) => e.type === 'Literal' && e.raw) &&
propertyNames.includes(node.key.name) &&
context.getAncestors().some((a) => isFlag(a))
ancestorsContainsSfCommand(ancestors) &&
ancestors.some((a) => isFlag(a))
) {
const flagsNode = context.getAncestors().find((a) => isFlagsStaticProperty(a));
const flagsNode = ancestors.find((a) => isFlagsStaticProperty(a));

const arrayValues = node.value.elements
.map((e) => (e.type === 'Literal' ? e.value : undefined))
Expand Down
44 changes: 44 additions & 0 deletions src/rules/flagSummary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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 { ESLintUtils } from '@typescript-eslint/utils';
import { ancestorsContainsSfCommand, isInCommandDirectory } from '../shared/commands';
import { flagPropertyIsNamed, isFlag } from '../shared/flags';

export const flagSummary = ESLintUtils.RuleCreator.withoutDocs({
meta: {
docs: {
description: 'Enforce that flags have a summary property',
recommended: 'error',
},
messages: {
message: 'Flags should have a summary property',
},
type: 'problem',
schema: [],
},
defaultOptions: [],
create(context) {
return {
Property(node): void {
if (isInCommandDirectory(context) && isFlag(node) && ancestorsContainsSfCommand(context.getAncestors())) {
if (
node.value?.type === 'CallExpression' &&
node.value.arguments?.[0]?.type === 'ObjectExpression' &&
!node.value.arguments[0].properties.some(
(property) => property.type === 'Property' && flagPropertyIsNamed(property, 'summary')
)
) {
context.report({
node,
messageId: 'message',
});
}
}
},
};
},
});
28 changes: 28 additions & 0 deletions src/shared/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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 { sep } from 'path';
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
import { RuleContext } from '@typescript-eslint/utils/dist/ts-eslint';

export const isClassDeclaration = (node: TSESTree.Node): node is TSESTree.ClassDeclaration =>
node.type === AST_NODE_TYPES.ClassDeclaration;

export const ancestorsContainsSfCommand = (ancestors: TSESTree.Node[]): boolean =>
ancestors.some((a) => isClassDeclaration(a) && extendsSfCommand(a));

export const extendsSfCommand = (node: TSESTree.ClassDeclaration): boolean =>
node.superClass?.type === AST_NODE_TYPES.Identifier && node.superClass.name === 'SfCommand';

export const getClassPropertyIdentifierName = (node: TSESTree.ClassElement): string =>
node.type === 'PropertyDefinition' && node.key.type === AST_NODE_TYPES.Identifier ? node.key.name : undefined;

// we don't care what the types are, really any context will do
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isInCommandDirectory = (context: RuleContext<any, any>): boolean => {
return context.getPhysicalFilename().includes(`${sep}commands${sep}`); // not an sfCommand
};
15 changes: 15 additions & 0 deletions src/shared/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,32 @@ export const getFlagName = (node: TSESTree.Node): string => {
}
};

/** Current node is 'foo' : Flags.x({}) */
export const isFlag = (node: TSESTree.Node): boolean =>
node.type === AST_NODE_TYPES.Property &&
node.value?.type === 'CallExpression' &&
node.value?.callee?.type === 'MemberExpression' &&
node.value?.callee?.object?.type === 'Identifier' &&
node.value?.callee?.object?.name === 'Flags';

/** Current node is public static flags = */
export const isFlagsStaticProperty = (node: TSESTree.Node): boolean =>
node.type === AST_NODE_TYPES.PropertyDefinition &&
node.static &&
node.value?.type === AST_NODE_TYPES.ObjectExpression &&
node.key.type === AST_NODE_TYPES.Identifier &&
node.key.name === 'flags' &&
node.accessibility === 'public';

export const flagPropertyIsNamed = (node: TSESTree.Property, name: string): node is TSESTree.Property =>
resolveFlagName(node) === name;

/** pass in a flag Property and it gives back the key name/value depending on type */
export const resolveFlagName = (flag: TSESTree.PropertyComputedName | TSESTree.PropertyNonComputedName): string => {
if (flag.key.type === AST_NODE_TYPES.Identifier) {
return flag.key.name;
}
if (flag.key.type === AST_NODE_TYPES.Literal && typeof flag.key.value === 'string') {
return flag.key.value;
}
};
29 changes: 27 additions & 2 deletions test/rules/duplicateChars.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ const ruleTester = new ESLintUtils.RuleTester({
ruleTester.run('no duplicate short characters', noDuplicateShortCharacters, {
valid: [
// example with different chars
`
{
filename: 'src/commands/foo.ts',
code: `
export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
public static flags = {
alias: Flags.string({
Expand All @@ -27,11 +29,32 @@ export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
}
`,
},
// example with some chars not present
`
{
filename: 'src/commands/foo.ts',
code: `
export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
public static flags = {
alias: Flags.string({
}),
'some-literal': Flags.string({
char: 'b'
}),
}
}
`,
},

// bad but not in commands directory
{
filename: 'src/foo.ts',
code: `
export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
public static flags = {
alias: Flags.string({
char: 'b'
}),
'some-literal': Flags.string({
char: 'b'
Expand All @@ -40,6 +63,7 @@ export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
}
`,
},
],
invalid: [
{
Expand All @@ -49,6 +73,7 @@ export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
data: { flag2: 'alias', flag1: 'some-literal', char: "'a'" },
},
],
filename: 'src/commands/foo.ts',
code: `
export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
public static flags = {
Expand Down
40 changes: 36 additions & 4 deletions test/rules/extractMessage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ const ruleTester = new ESLintUtils.RuleTester({
ruleTester.run('no duplicate short characters', extractMessage, {
valid: [
// no messages is fine
`
{
filename: 'src/commands/foo.ts',
code: `
export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
public static flags = {
alias: Flags.string({
Expand All @@ -23,8 +25,11 @@ export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
}
}
`,
},
// summary only
`
{
filename: 'src/commands/foo.ts',
code: `
export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
public static flags = {
alias: Flags.string({
Expand All @@ -33,7 +38,10 @@ export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
}
}
`,
`
},
{
filename: 'src/commands/foo.ts',
code: `
export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
public static flags = {
alias: Flags.string({
Expand All @@ -43,7 +51,10 @@ export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
}
}
`,
`
},
{
filename: 'src/commands/foo.ts',
code: `
export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
public static flags = {
alias: Flags.string({
Expand All @@ -52,9 +63,26 @@ export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
}
}
`,
},
// all sorts of violations but not in the commands directory
{
filename: 'src/foo.ts',
code: `
export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
public static flags = {
alias: Flags.string({
description: 'foo',
summary: 'foo'
}),
}
}
`,
},
],
invalid: [
{
filename: 'src/commands/foo.ts',

errors: [
{
messageId: 'message',
Expand All @@ -76,6 +104,8 @@ export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
messageId: 'message',
},
],
filename: 'src/commands/foo.ts',

code: `
export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
public static flags = {
Expand All @@ -95,6 +125,8 @@ export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
messageId: 'message',
},
],
filename: 'src/commands/foo.ts',

code: `
export default class EnvCreateScratch extends SfCommand<ScratchCreateResponse> {
public static flags = {
Expand Down
Loading

0 comments on commit 1c04f52

Please sign in to comment.