Skip to content

Commit

Permalink
dev: add eslint rule for annotation checks
Browse files Browse the repository at this point in the history
Inversify >=6.1 requires to annotate all constructor parameters of
injectable classes. To avoid runtime errors we add a new eslint rule to
check for this at lint time.
  • Loading branch information
sdirix committed Nov 27, 2024
1 parent 41ba18e commit 5554acb
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 0 deletions.
1 change: 1 addition & 0 deletions configs/errors.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
}
}
],
"@theia/annotation-check": "error",
"@theia/localization-check": "error",
"@theia/no-src-import": "error",
"@theia/runtime-import-check": "error",
Expand Down
5 changes: 5 additions & 0 deletions dev-packages/private-eslint-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ The plugin helps identify problems during development through static analysis in

## Rules

### `annotation-check`:

Inversify >=6.1 requires to annotate all constructor parameters of injectable classes as otherwise runtime errors are thrown.
The rule checks that all constructor parameters of injectable classes are annotated with `@inject`, `@unmanaged` or `@multiInject`.

### `localization-check`:

The rule prevents the following localization related issues:
Expand Down
1 change: 1 addition & 0 deletions dev-packages/private-eslint-plugin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

/** @type {{[ruleId: string]: import('eslint').Rule.RuleModule}} */
exports.rules = {
"annotation-check": require('./rules/annotation-check'),
"localization-check": require('./rules/localization-check'),
"no-src-import": require('./rules/no-src-import'),
"runtime-import-check": require('./rules/runtime-import-check'),
Expand Down
103 changes: 103 additions & 0 deletions dev-packages/private-eslint-plugin/rules/annotation-check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// @ts-check
// *****************************************************************************
// Copyright (C) 2024 EclipseSource and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

/**
* @typedef {import('@typescript-eslint/utils').TSESTree.ClassDeclaration} ClassDeclaration
* @typedef {import('@typescript-eslint/utils').TSESTree.ClassElement} ClassElement
* @typedef {import('@typescript-eslint/utils').TSESTree.Decorator} Decorator
* @typedef {import('@typescript-eslint/utils').TSESTree.MethodDefinition} MethodDefinition
* @typedef {import('@typescript-eslint/utils').TSESTree.Parameter} Parameter
* @typedef {import('estree').Node} Node
* @typedef {import('eslint').Rule.RuleModule} RuleModule
*/

/**
* Type guard to check if a ClassElement is a MethodDefinition.
* @param {ClassElement} element
* @returns {element is MethodDefinition}
*/
function isMethodDefinition(element) {
return element.type === 'MethodDefinition';
}

/** @type {RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'Ensure @injectable classes have annotated constructor parameters',
},
messages: {
missingAnnotation: 'Constructor parameters in an @injectable class must be annotated with @inject, @unmanaged or @multiInject',
},
},
create(context) {
return {
/**
* @param {ClassDeclaration} node
*/
ClassDeclaration(node) {
// Check if the class has a decorator named `injectable`
const hasInjectableDecorator = node.decorators?.some(
(/** @type {Decorator} */ decorator) =>
decorator.expression.type === 'CallExpression' &&
decorator.expression.callee.type === 'Identifier' &&
decorator.expression.callee.name === 'injectable'
);

if (hasInjectableDecorator) {
// Find the constructor method within the class body
const constructor = node.body.body.find(
member =>
isMethodDefinition(member) &&
member.kind === 'constructor'
);

if (
constructor &&
// We need to re-apply 'isMethodDefinition' here because the type guard is not properly preserved
isMethodDefinition(constructor) &&
constructor.value &&
constructor.value.params.length > 0
) {
constructor.value.params.forEach(
/** @type {Parameter} */ param => {
// Check if each constructor parameter has a decorator
const hasAnnotation = param.decorators?.some(
(/** @type {Decorator} */ decorator) =>
decorator.expression.type === 'CallExpression' &&
decorator.expression.callee.type === 'Identifier' &&
(decorator.expression.callee.name === 'inject' ||
decorator.expression.callee.name === 'unmanaged' ||
decorator.expression.callee.name === 'multiInject')
);

if (!hasAnnotation) {
context.report({
node: /** @type Node */ (param),
messageId: 'missingAnnotation',
});
}
}
);
}
}
},
};
},
};

0 comments on commit 5554acb

Please sign in to comment.