-
-
Notifications
You must be signed in to change notification settings - Fork 630
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: implement ESLint rules for correct CC implementations
- Loading branch information
Showing
5 changed files
with
181 additions
and
25 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 |
---|---|---|
@@ -1,9 +1,11 @@ | ||
import { ccAPIValidateArgs } from "./rules/ccapi-validate-args.js"; | ||
import { consistentCCClasses } from "./rules/consistent-cc-classes.js"; | ||
import { noDebugInTests } from "./rules/no-debug-in-tests.js"; | ||
|
||
module.exports = { | ||
rules: { | ||
"no-debug-in-tests": noDebugInTests, | ||
"ccapi-validate-args": ccAPIValidateArgs, | ||
"consistent-cc-classes": consistentCCClasses, | ||
}, | ||
}; |
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
120 changes: 120 additions & 0 deletions
120
packages/eslint-plugin/src/rules/consistent-cc-classes.ts
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,120 @@ | ||
import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils"; | ||
import { | ||
findDecorator, | ||
findDecoratorContainingCCId, | ||
getCCNameFromDecorator, | ||
} from "../utils"; | ||
|
||
export const consistentCCClasses = ESLintUtils.RuleCreator.withoutDocs({ | ||
create(context) { | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
let currentCCName: string | undefined; | ||
|
||
return { | ||
ClassDeclaration(node) { | ||
// Only look at class declarations ending with "CC" | ||
if (!node.id?.name.endsWith("CC")) return; | ||
// Except InvalidCC, which is a special case | ||
if (node.id?.name === "InvalidCC") return; | ||
|
||
// These must... | ||
|
||
// ...be in a file that ends with "CC.ts" | ||
if (!context.getFilename().endsWith("CC.ts")) { | ||
context.report({ | ||
node, | ||
loc: node.id.loc, | ||
messageId: "wrong-filename", | ||
}); | ||
} | ||
|
||
// ...have an @commandClass decorator | ||
const ccDecorator = findDecoratorContainingCCId(node, [ | ||
"commandClass", | ||
]); | ||
if (!ccDecorator) { | ||
context.report({ | ||
node, | ||
loc: node.id.loc, | ||
messageId: "missing-cc-decorator", | ||
}); | ||
} else { | ||
currentCCName = getCCNameFromDecorator(ccDecorator); | ||
} | ||
|
||
// ...have a @implementedVersion decorator | ||
const versionDecorator = findDecorator( | ||
node, | ||
"implementedVersion", | ||
); | ||
if (!versionDecorator) { | ||
context.report({ | ||
node, | ||
loc: node.id.loc, | ||
messageId: "missing-version-decorator", | ||
}); | ||
} | ||
|
||
// ...be exported | ||
if ( | ||
node.parent.type !== AST_NODE_TYPES.ExportNamedDeclaration | ||
|| node.parent.exportKind !== "value" | ||
) { | ||
context.report({ | ||
node, | ||
loc: node.id.loc, | ||
messageId: "must-export", | ||
fix: (fixer) => fixer.insertTextBefore(node, "export "), | ||
}); | ||
} | ||
|
||
// ...inherit from CommandClass | ||
if ( | ||
!node.superClass | ||
|| node.superClass.type !== AST_NODE_TYPES.Identifier | ||
|| node.superClass.name !== "CommandClass" | ||
) { | ||
context.report({ | ||
node, | ||
loc: node.id.loc, | ||
fix: (fixer) => | ||
node.superClass | ||
? fixer.replaceText( | ||
node.superClass, | ||
"CommandClass", | ||
) | ||
: fixer.insertTextAfter( | ||
node.id!, | ||
" extends CommandClass", | ||
), | ||
messageId: "must-inherit-commandclass", | ||
}); | ||
} | ||
}, | ||
"ClassDeclaration:exit"(_node) { | ||
currentCCName = undefined; | ||
}, | ||
}; | ||
}, | ||
meta: { | ||
docs: { | ||
description: | ||
"Ensures that CC implementations follow certain conventions.", | ||
}, | ||
type: "problem", | ||
schema: [], | ||
fixable: "code", | ||
messages: { | ||
"wrong-filename": | ||
"Classes that end with `CC` are considered CC implementations and MUST be in a file whose name ends with `CC.ts`", | ||
"missing-cc-decorator": | ||
"Classes implementing a CC must have a CC assigned using the `@commandClass(...)` decorator", | ||
"missing-version-decorator": | ||
"Classes implementing a CC must be decorated with `@implementedVersion(...)`", | ||
"must-export": "Classes implementing a CC must be exported", | ||
"must-inherit-commandclass": | ||
"Classes implementing a CC MUST inherit from `CommandClass`", | ||
}, | ||
}, | ||
defaultOptions: [], | ||
}); |
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 |
---|---|---|
@@ -1,5 +1,55 @@ | ||
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils"; | ||
import path from "node:path"; | ||
|
||
export const repoRoot = path.normalize( | ||
__dirname.slice(0, __dirname.lastIndexOf(`${path.sep}packages${path.sep}`)), | ||
); | ||
|
||
/** Finds a specific decorator on a node */ | ||
export function findDecorator( | ||
node: TSESTree.ClassDeclaration, | ||
name: string, | ||
): TSESTree.Decorator | undefined { | ||
return node.decorators.find((d) => | ||
d.expression.type === AST_NODE_TYPES.CallExpression | ||
&& d.expression.callee.type === AST_NODE_TYPES.Identifier | ||
&& d.expression.callee.name === name | ||
); | ||
} | ||
|
||
/** Finds the `@API(...)` or `@commandClass(...)` decorator on a node */ | ||
export function findDecoratorContainingCCId( | ||
node: TSESTree.ClassDeclaration, | ||
possibleNames: string[] = ["API", "commandClass"], | ||
): TSESTree.Decorator | undefined { | ||
return node.decorators.find((d) => | ||
d.expression.type === AST_NODE_TYPES.CallExpression | ||
&& d.expression.callee.type === AST_NODE_TYPES.Identifier | ||
&& possibleNames.includes(d.expression.callee.name) | ||
&& d.expression.arguments.length === 1 | ||
&& d.expression.arguments[0].type | ||
=== AST_NODE_TYPES.MemberExpression | ||
&& d.expression.arguments[0].object.type | ||
=== AST_NODE_TYPES.Identifier | ||
&& d.expression.arguments[0].object.name | ||
=== "CommandClasses" | ||
&& (d.expression.arguments[0].property.type | ||
=== AST_NODE_TYPES.Identifier | ||
|| (d.expression.arguments[0].property.type | ||
=== AST_NODE_TYPES.Literal | ||
&& typeof d.expression.arguments[0].property.value | ||
=== "string")) | ||
); | ||
} | ||
|
||
/** Takes a decorator found using {@link findDecoratorContainingCCId} and returns the CC name */ | ||
export function getCCNameFromDecorator( | ||
decorator: TSESTree.Decorator, | ||
): string { | ||
const prop: TSESTree.Literal | TSESTree.Identifier = | ||
(decorator.expression as any).arguments[0].property; | ||
|
||
return prop.type === AST_NODE_TYPES.Literal | ||
? prop.value as string | ||
: prop.name; | ||
} |