@angular-eslint/eslint-plugin
Version:
ESLint plugin for Angular applications, following https://angular.dev/style-guide
175 lines (174 loc) • 9.14 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_DOCS_EXTENSION = exports.RULE_NAME = void 0;
const utils_1 = require("@angular-eslint/utils");
const utils_2 = require("@typescript-eslint/utils");
const create_eslint_rule_1 = require("../utils/create-eslint-rule");
const signals_1 = require("../utils/signals");
const DEFAULT_OPTIONS = {
preferReadonlySignalProperties: true,
preferInputSignals: true,
preferQuerySignals: true,
useTypeChecking: false,
additionalSignalCreationFunctions: [],
};
const KNOWN_SIGNAL_CREATION_FUNCTIONS = new Set([
'computed',
'contentChild',
'contentChildren',
'input',
'linkedSignal',
'model',
'signal',
'toSignal',
'viewChild',
'viewChildren',
]);
exports.RULE_NAME = 'prefer-signals';
exports.default = (0, create_eslint_rule_1.createESLintRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Use readonly signals instead of `@Input()`, `@ViewChild()` and other legacy decorators',
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
preferReadonlySignalProperties: {
type: 'boolean',
default: DEFAULT_OPTIONS.preferReadonlySignalProperties,
},
preferInputSignals: {
type: 'boolean',
default: DEFAULT_OPTIONS.preferInputSignals,
},
preferQuerySignals: {
type: 'boolean',
default: DEFAULT_OPTIONS.preferQuerySignals,
},
useTypeChecking: {
type: 'boolean',
default: DEFAULT_OPTIONS.useTypeChecking,
},
additionalSignalCreationFunctions: {
type: 'array',
items: { type: 'string' },
default: DEFAULT_OPTIONS.additionalSignalCreationFunctions,
},
},
additionalProperties: false,
},
],
messages: {
preferInputSignals: 'Use `InputSignal`s (e.g. via `input()`) for Component input properties rather than the legacy `@Input()` decorator',
preferQuerySignals: 'Use the `{{function}}` function instead of the `{{decorator}}` decorator',
preferReadonlySignalProperties: 'Properties declared using signals should be marked as `readonly` since they should not be reassigned',
},
},
defaultOptions: [{ ...DEFAULT_OPTIONS }],
create(context, [{ preferReadonlySignalProperties = DEFAULT_OPTIONS.preferReadonlySignalProperties, preferInputSignals = DEFAULT_OPTIONS.preferInputSignals, preferQuerySignals = DEFAULT_OPTIONS.preferQuerySignals, additionalSignalCreationFunctions = DEFAULT_OPTIONS.additionalSignalCreationFunctions, useTypeChecking = DEFAULT_OPTIONS.useTypeChecking, },]) {
let services;
const listener = {};
if (preferReadonlySignalProperties) {
listener[`PropertyDefinition:not([readonly=true])`] = (node) => {
let shouldBeReadonly = false;
if (node.typeAnnotation) {
// Use the type annotation to determine
// whether the property is a signal.
if (node.typeAnnotation.typeAnnotation.type ===
utils_2.AST_NODE_TYPES.TSTypeReference) {
const type = node.typeAnnotation.typeAnnotation;
if (type.typeArguments &&
type.typeName.type === utils_2.AST_NODE_TYPES.Identifier &&
signals_1.KNOWN_SIGNAL_TYPES.has(type.typeName.name)) {
shouldBeReadonly = true;
}
}
}
else {
// There is no type annotation, so try to
// use the value assigned to the property
// to determine whether it would be a signal.
let value = node.value;
if (value?.type === utils_2.AST_NODE_TYPES.CallExpression) {
const callee = value.callee;
// A `WritableSignal` can be turned into a `Signal` using
// the `.asReadonly()` method. If that method is being
// called, then we need to look at the object that the method
// is called on to determine if it's being called on a `Signal`.
if (callee.type === utils_2.AST_NODE_TYPES.MemberExpression) {
if (callee.property.type === utils_2.AST_NODE_TYPES.Identifier &&
callee.property.name === 'asReadonly') {
value = callee.object;
}
}
}
if (value?.type === utils_2.AST_NODE_TYPES.CallExpression) {
let callee = value.callee;
// Some signal-creating functions have a `.required`
// member. For example, `input.required()`.
if (callee.type === utils_2.AST_NODE_TYPES.MemberExpression) {
if (callee.property.type === utils_2.AST_NODE_TYPES.Identifier &&
callee.property.name !== 'required') {
return;
}
callee = callee.object;
}
if (callee.type === utils_2.AST_NODE_TYPES.Identifier &&
(KNOWN_SIGNAL_CREATION_FUNCTIONS.has(callee.name) ||
additionalSignalCreationFunctions.includes(callee.name))) {
shouldBeReadonly = true;
}
}
if (!shouldBeReadonly && useTypeChecking && node.value) {
services ??= utils_2.ESLintUtils.getParserServices(context);
const name = services
.getTypeAtLocation(node.value)
.getSymbol()?.name;
shouldBeReadonly =
name !== undefined && signals_1.KNOWN_SIGNAL_TYPES.has(name);
}
}
if (shouldBeReadonly) {
context.report({
node: node.key,
messageId: 'preferReadonlySignalProperties',
fix: (fixer) => fixer.insertTextBefore(node.key, 'readonly '),
});
}
};
}
if (preferInputSignals) {
listener[utils_1.Selectors.INPUT_DECORATOR] = (node) => {
context.report({
node,
messageId: 'preferInputSignals',
});
};
}
if (preferQuerySignals) {
listener['Decorator[expression.callee.name=/^(ContentChild|ContentChildren|ViewChild|ViewChildren)$/]'] = (node) => {
if (node.expression.type === utils_2.AST_NODE_TYPES.CallExpression &&
node.expression.callee.type === utils_2.AST_NODE_TYPES.Identifier) {
const decoratorName = node.expression.callee.name;
context.report({
node,
messageId: 'preferQuerySignals',
data: {
function: decoratorName.slice(0, 1).toLowerCase() +
decoratorName.slice(1),
decorator: decoratorName,
},
});
}
};
}
return listener;
},
});
exports.RULE_DOCS_EXTENSION = {
rationale: "Angular signals represent the future of reactivity in Angular, offering fine-grained change detection, better performance, and improved developer experience. Signal-based APIs like input(), viewChild(), and contentChild() provide type-safe, reactive properties that integrate seamlessly with computed values and effects. Unlike decorator-based APIs (@Input(), @ViewChild(), etc.), signals enable more granular tracking of dependencies and updates, allowing Angular to optimize change detection. Signal properties should be marked readonly because signals themselves are stable references - you read their value by calling them, you don't reassign the signal. This prevents bugs where developers might accidentally reassign a signal instead of updating its value. Using signals throughout your components creates a consistent, reactive programming model that makes data flow explicit and easier to understand.",
};