@depup/angular-eslint__eslint-plugin 22.1.0-depup.1 → 22.3.0-depup.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/changes.json +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/rules/component-class-suffix.js +5 -5
- package/dist/rules/component-max-inline-declarations.js +7 -7
- package/dist/rules/computed-must-return.js +1 -1
- package/dist/rules/consistent-component-styles.js +1 -1
- package/dist/rules/contextual-decorator.js +1 -1
- package/dist/rules/contextual-lifecycle.js +1 -1
- package/dist/rules/directive-class-suffix.js +1 -1
- package/dist/rules/inject-at-top.d.ts +6 -2
- package/dist/rules/inject-at-top.d.ts.map +1 -1
- package/dist/rules/inject-at-top.js +53 -22
- package/dist/rules/no-async-lifecycle-method.js +1 -1
- package/dist/rules/no-attribute-decorator.js +1 -1
- package/dist/rules/no-developer-preview.js +1 -1
- package/dist/rules/no-duplicates-in-metadata-arrays.d.ts.map +1 -1
- package/dist/rules/no-duplicates-in-metadata-arrays.js +11 -6
- package/dist/rules/no-empty-lifecycle-method.js +1 -1
- package/dist/rules/no-experimental.js +1 -1
- package/dist/rules/no-forward-ref.js +1 -1
- package/dist/rules/no-implicit-take-until-destroyed.d.ts.map +1 -1
- package/dist/rules/no-implicit-take-until-destroyed.js +16 -6
- package/dist/rules/no-input-prefix.js +1 -1
- package/dist/rules/no-input-rename.js +1 -1
- package/dist/rules/no-inputs-metadata-property.js +1 -1
- package/dist/rules/no-lifecycle-call.js +1 -1
- package/dist/rules/no-output-native.js +1 -1
- package/dist/rules/no-output-on-prefix.js +1 -1
- package/dist/rules/no-output-rename.js +1 -1
- package/dist/rules/no-outputs-metadata-property.js +1 -1
- package/dist/rules/no-pipe-impure.js +1 -1
- package/dist/rules/no-queries-metadata-property.js +1 -1
- package/dist/rules/no-uncalled-signals.js +1 -1
- package/dist/rules/pipe-prefix.js +5 -5
- package/dist/rules/prefer-host-metadata-property.js +1 -1
- package/dist/rules/prefer-inject.js +1 -1
- package/dist/rules/prefer-on-push-component-change-detection.d.ts +6 -3
- package/dist/rules/prefer-on-push-component-change-detection.d.ts.map +1 -1
- package/dist/rules/prefer-on-push-component-change-detection.js +50 -20
- package/dist/rules/prefer-output-emitter-ref.js +1 -1
- package/dist/rules/prefer-output-readonly.js +1 -1
- package/dist/rules/prefer-service-decorator.js +1 -1
- package/dist/rules/prefer-signal-model.d.ts +7 -1
- package/dist/rules/prefer-signal-model.d.ts.map +1 -1
- package/dist/rules/prefer-signal-model.js +118 -40
- package/dist/rules/prefer-signals.js +1 -1
- package/dist/rules/prefer-standalone.js +1 -1
- package/dist/rules/reactive-context-must-read-signal.d.ts +16 -0
- package/dist/rules/reactive-context-must-read-signal.d.ts.map +1 -0
- package/dist/rules/reactive-context-must-read-signal.js +357 -0
- package/dist/rules/relative-url-prefix.js +1 -1
- package/dist/rules/require-lifecycle-on-prototype.js +1 -1
- package/dist/rules/require-localize-metadata.js +1 -1
- package/dist/rules/runtime-localize.js +1 -1
- package/dist/rules/sort-keys-in-type-decorator.js +1 -1
- package/dist/rules/sort-lifecycle-methods.js +1 -1
- package/dist/rules/use-component-selector.js +1 -1
- package/dist/rules/use-component-view-encapsulation.js +1 -1
- package/dist/rules/use-injectable-provided-in.d.ts +6 -3
- package/dist/rules/use-injectable-provided-in.d.ts.map +1 -1
- package/dist/rules/use-injectable-provided-in.js +14 -5
- package/dist/rules/use-lifecycle-interface.js +1 -1
- package/dist/rules/use-pipe-transform-interface.js +1 -1
- package/dist/utils/create-eslint-rule.d.ts.map +1 -1
- package/dist/utils/create-eslint-rule.js +1 -24
- package/package.json +7 -7
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.RULE_DOCS_EXTENSION = exports.RULE_NAME = void 0;
|
|
40
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
41
|
+
const tsutils = __importStar(require("ts-api-utils"));
|
|
42
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
43
|
+
const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
44
|
+
const signals_1 = require("../utils/signals");
|
|
45
|
+
exports.RULE_NAME = 'reactive-context-must-read-signal';
|
|
46
|
+
/**
|
|
47
|
+
* Reactive contexts that should depend on at least one signal. The tracked
|
|
48
|
+
* function is the one whose signal reads define the context's dependencies:
|
|
49
|
+
* - `computed`/`effect`: the first argument.
|
|
50
|
+
* - `linkedSignal`: the first argument (shorthand) or the `source`/`computation`
|
|
51
|
+
* functions of the options object.
|
|
52
|
+
* - `afterRenderEffect`: the first argument (shorthand) or the phase functions
|
|
53
|
+
* of the options object.
|
|
54
|
+
*/
|
|
55
|
+
const DEFAULT_PRIMITIVES = {
|
|
56
|
+
afterRenderEffect: {
|
|
57
|
+
args: [0],
|
|
58
|
+
optionKeys: ['earlyRead', 'write', 'mixedReadWrite', 'read'],
|
|
59
|
+
},
|
|
60
|
+
computed: { args: [0], optionKeys: [] },
|
|
61
|
+
effect: { args: [0], optionKeys: [] },
|
|
62
|
+
linkedSignal: { args: [0], optionKeys: ['source', 'computation'] },
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* For resources, only the `params` (formerly `request`) function defines the
|
|
66
|
+
* reactive dependencies; reading a signal in the `loader`/`stream` does not make
|
|
67
|
+
* the resource reload. These are opt-in via the `checkResources` option.
|
|
68
|
+
*/
|
|
69
|
+
const RESOURCE_PRIMITIVES = {
|
|
70
|
+
resource: { args: [0], optionKeys: ['params', 'request'] },
|
|
71
|
+
rxResource: { args: [0], optionKeys: ['params', 'request'] },
|
|
72
|
+
};
|
|
73
|
+
function isFunction(node) {
|
|
74
|
+
return (node?.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
75
|
+
node?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression);
|
|
76
|
+
}
|
|
77
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
78
|
+
name: exports.RULE_NAME,
|
|
79
|
+
meta: {
|
|
80
|
+
type: 'problem',
|
|
81
|
+
docs: {
|
|
82
|
+
description: 'Ensures that reactive contexts such as computed(), linkedSignal() and effect() read at least one reactive value (a signal). A reactive context with no reactive dependencies never re-runs, which is almost always a mistake.',
|
|
83
|
+
},
|
|
84
|
+
schema: [
|
|
85
|
+
{
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
checkResources: {
|
|
89
|
+
type: 'boolean',
|
|
90
|
+
description: 'Also check that the `params` function of `resource()` and `rxResource()` reads a signal.',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
additionalProperties: false,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
messages: {
|
|
97
|
+
mustReadSignal: '{{primitive}}() must read at least one signal, otherwise it will never re-run when state changes.',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
defaultOptions: [{ checkResources: false }],
|
|
101
|
+
create(context, [{ checkResources = false }]) {
|
|
102
|
+
const services = utils_1.ESLintUtils.getParserServices(context);
|
|
103
|
+
const checker = services.program.getTypeChecker();
|
|
104
|
+
const primitives = checkResources
|
|
105
|
+
? { ...DEFAULT_PRIMITIVES, ...RESOURCE_PRIMITIVES }
|
|
106
|
+
: DEFAULT_PRIMITIVES;
|
|
107
|
+
const analyses = new Map();
|
|
108
|
+
const trackedFnToCall = new Map();
|
|
109
|
+
const frameStack = [];
|
|
110
|
+
/**
|
|
111
|
+
* Returns true if calling `callee` reads a signal, i.e. `callee` has one of
|
|
112
|
+
* the known signal types. This mirrors the detection used by
|
|
113
|
+
* `no-uncalled-signals`.
|
|
114
|
+
*/
|
|
115
|
+
function isSignalRead(callee) {
|
|
116
|
+
const type = services.getTypeAtLocation(callee);
|
|
117
|
+
const symbol = type.getSymbol();
|
|
118
|
+
if (symbol && signals_1.KNOWN_SIGNAL_TYPES.has(symbol.name)) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
// The `Signal` type is a type alias, so it is only reachable via the
|
|
122
|
+
// alias symbol. Other signal types (e.g. `InputSignal`) are interfaces
|
|
123
|
+
// and are found through `getSymbol()` above.
|
|
124
|
+
if (type.aliasSymbol && signals_1.KNOWN_SIGNAL_TYPES.has(type.aliasSymbol.name)) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Returns true if the symbol is only declared in TypeScript's default
|
|
131
|
+
* library files. Those declarations (arrays, strings, `Math`, `JSON`,
|
|
132
|
+
* `Object`, `console`, DOM APIs, ...) cannot read an Angular signal.
|
|
133
|
+
*/
|
|
134
|
+
function isDeclaredInDefaultLib(symbol) {
|
|
135
|
+
const declarations = symbol.getDeclarations();
|
|
136
|
+
return (!!declarations?.length &&
|
|
137
|
+
declarations.every((declaration) => services.program.isSourceFileDefaultLibrary(declaration.getSourceFile())));
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Returns true if the node is a function-typed value that we cannot look
|
|
141
|
+
* inside, e.g. `this.transform` in `this.items.map(this.transform)`. The
|
|
142
|
+
* callee could invoke it, and it could read a signal.
|
|
143
|
+
*/
|
|
144
|
+
function isOpaqueFunctionValue(node) {
|
|
145
|
+
if (isFunction(node)) {
|
|
146
|
+
// Inline functions are visited by this rule, so any signal they read is
|
|
147
|
+
// attributed to the enclosing reactive context.
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
if (node.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
151
|
+
// We cannot tell what a spread expands to, so assume the worst.
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
const type = services.getTypeAtLocation(node);
|
|
155
|
+
if (tsutils.isTypeFlagSet(type, typescript_1.default.TypeFlags.Any | typescript_1.default.TypeFlags.Unknown)) {
|
|
156
|
+
// We know nothing about the value, so it could well be a function
|
|
157
|
+
// that reads a signal.
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
return tsutils
|
|
161
|
+
.unionConstituents(type)
|
|
162
|
+
.some((constituent) => constituent.getCallSignatures().length > 0);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Returns true if the property with the given name is a getter on any
|
|
166
|
+
* constituent of the type. A getter runs arbitrary code, so it can read a
|
|
167
|
+
* signal even though reading the property does not look like a call.
|
|
168
|
+
*/
|
|
169
|
+
function isGetter(type, name) {
|
|
170
|
+
return tsutils.unionConstituents(type).some((constituent) => {
|
|
171
|
+
const property = constituent.getProperty(name);
|
|
172
|
+
return (property !== undefined &&
|
|
173
|
+
tsutils.isSymbolFlagSet(property, typescript_1.default.SymbolFlags.GetAccessor));
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Records what a call, `new` expression or tagged template tells us about
|
|
178
|
+
* the reactive context we are currently inside. It is only known not to
|
|
179
|
+
* read a signal when the callee resolves to a declaration in a TypeScript
|
|
180
|
+
* default library and no argument is an opaque function value.
|
|
181
|
+
*/
|
|
182
|
+
function analyseCallLike(callee, args, analysis) {
|
|
183
|
+
let symbol = services.getSymbolAtLocation(callee);
|
|
184
|
+
if (symbol && tsutils.isSymbolFlagSet(symbol, typescript_1.default.SymbolFlags.Alias)) {
|
|
185
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
186
|
+
}
|
|
187
|
+
if (!symbol ||
|
|
188
|
+
!isDeclaredInDefaultLib(symbol) ||
|
|
189
|
+
args.some(isOpaqueFunctionValue)) {
|
|
190
|
+
analysis.hasUnknown = true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** The analysis of the reactive context we are currently inside, if any. */
|
|
194
|
+
function currentAnalysis() {
|
|
195
|
+
const top = frameStack[frameStack.length - 1];
|
|
196
|
+
return top && analyses.get(top.call);
|
|
197
|
+
}
|
|
198
|
+
function registerTrackedFunction(node, call, analysis) {
|
|
199
|
+
if (isFunction(node)) {
|
|
200
|
+
trackedFnToCall.set(node, call);
|
|
201
|
+
analysis.hasTrackedFn = true;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// The tracked slot holds something we cannot look inside, such as a
|
|
205
|
+
// signal passed directly as `source`, or a reference to a function
|
|
206
|
+
// declared elsewhere. It may well be reactive, so stay silent.
|
|
207
|
+
analysis.hasUnknown = true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function registerTrackedOptions(options, call, config, analysis) {
|
|
211
|
+
for (const property of options.properties) {
|
|
212
|
+
if (property.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
213
|
+
// The spread could provide any of the tracked options.
|
|
214
|
+
analysis.hasUnknown = true;
|
|
215
|
+
}
|
|
216
|
+
else if (!property.computed &&
|
|
217
|
+
property.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
218
|
+
config.optionKeys.includes(property.key.name)) {
|
|
219
|
+
registerTrackedFunction(property.value, call, analysis);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function registerTrackedFunctions(call, config, analysis) {
|
|
224
|
+
for (const index of config.args) {
|
|
225
|
+
const arg = call.arguments[index];
|
|
226
|
+
if (arg === undefined) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (config.optionKeys.length > 0 &&
|
|
230
|
+
arg.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
231
|
+
registerTrackedOptions(arg, call, config, analysis);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
registerTrackedFunction(arg, call, analysis);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function enterFunction(node) {
|
|
239
|
+
const call = trackedFnToCall.get(node);
|
|
240
|
+
if (call) {
|
|
241
|
+
frameStack.push({ fn: node, call });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function exitFunction(node) {
|
|
245
|
+
const top = frameStack[frameStack.length - 1];
|
|
246
|
+
if (top?.fn === node) {
|
|
247
|
+
frameStack.pop();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
ArrowFunctionExpression: enterFunction,
|
|
252
|
+
'ArrowFunctionExpression:exit': exitFunction,
|
|
253
|
+
FunctionExpression: enterFunction,
|
|
254
|
+
'FunctionExpression:exit': exitFunction,
|
|
255
|
+
MemberExpression(node) {
|
|
256
|
+
const analysis = currentAnalysis();
|
|
257
|
+
if (!analysis) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// A getter runs arbitrary code, so `this.name` can read a signal even
|
|
261
|
+
// though it does not look like a call.
|
|
262
|
+
const symbol = services.getSymbolAtLocation(node);
|
|
263
|
+
if (symbol &&
|
|
264
|
+
tsutils.isSymbolFlagSet(symbol, typescript_1.default.SymbolFlags.GetAccessor)) {
|
|
265
|
+
analysis.hasUnknown = true;
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
ObjectPattern(node) {
|
|
269
|
+
const analysis = currentAnalysis();
|
|
270
|
+
if (!analysis) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
// Destructuring invokes getters just like a member expression does,
|
|
274
|
+
// e.g. `const { double } = this`.
|
|
275
|
+
const type = services.getTypeAtLocation(node);
|
|
276
|
+
for (const property of node.properties) {
|
|
277
|
+
if (property.type !== utils_1.AST_NODE_TYPES.Property ||
|
|
278
|
+
property.computed ||
|
|
279
|
+
property.key.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
280
|
+
// A rest element copies every property and a computed or literal
|
|
281
|
+
// key can name any property, so assume a getter could be involved.
|
|
282
|
+
analysis.hasUnknown = true;
|
|
283
|
+
}
|
|
284
|
+
else if (isGetter(type, property.key.name)) {
|
|
285
|
+
analysis.hasUnknown = true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
NewExpression(node) {
|
|
290
|
+
const analysis = currentAnalysis();
|
|
291
|
+
if (analysis) {
|
|
292
|
+
analyseCallLike(node.callee, node.arguments, analysis);
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
TaggedTemplateExpression(node) {
|
|
296
|
+
const analysis = currentAnalysis();
|
|
297
|
+
if (analysis) {
|
|
298
|
+
analyseCallLike(node.tag, node.quasi.expressions, analysis);
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
CallExpression(node) {
|
|
302
|
+
// Attribute this call to the tracked function we are currently inside,
|
|
303
|
+
// if any. This runs before registration below, so a reactive context
|
|
304
|
+
// nested inside another tracked function is correctly treated as an
|
|
305
|
+
// (unknown) call of the outer function.
|
|
306
|
+
const analysis = currentAnalysis();
|
|
307
|
+
if (analysis) {
|
|
308
|
+
if (isSignalRead(node.callee)) {
|
|
309
|
+
analysis.hasReactiveRead = true;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
analyseCallLike(node.callee, node.arguments, analysis);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Register this call if it is one of the reactive contexts.
|
|
316
|
+
if (node.callee.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const config = primitives[node.callee.name];
|
|
320
|
+
if (!config) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const newAnalysis = {
|
|
324
|
+
call: node,
|
|
325
|
+
primitiveName: node.callee.name,
|
|
326
|
+
hasTrackedFn: false,
|
|
327
|
+
hasReactiveRead: false,
|
|
328
|
+
hasUnknown: false,
|
|
329
|
+
};
|
|
330
|
+
registerTrackedFunctions(node, config, newAnalysis);
|
|
331
|
+
analyses.set(node, newAnalysis);
|
|
332
|
+
},
|
|
333
|
+
'CallExpression:exit'(node) {
|
|
334
|
+
const analysis = analyses.get(node);
|
|
335
|
+
if (!analysis) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
analyses.delete(node);
|
|
339
|
+
// Only report when we are certain there is no reactivity: there is a
|
|
340
|
+
// tracked function, no signal was read, and there was nothing that
|
|
341
|
+
// might have read a signal on our behalf.
|
|
342
|
+
if (analysis.hasTrackedFn &&
|
|
343
|
+
!analysis.hasReactiveRead &&
|
|
344
|
+
!analysis.hasUnknown) {
|
|
345
|
+
context.report({
|
|
346
|
+
node: analysis.call,
|
|
347
|
+
messageId: 'mustReadSignal',
|
|
348
|
+
data: { primitive: analysis.primitiveName },
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
exports.RULE_DOCS_EXTENSION = {
|
|
356
|
+
rationale: 'Reactive contexts like `computed()`, `linkedSignal()`, `effect()` and `afterRenderEffect()` re-run whenever a signal they read changes. If the relevant function never reads a signal, the context runs once and can never react to anything, so it adds overhead without providing reactivity. This is usually a mistake: either the developer forgot to call a signal (e.g. wrote `firstName` instead of `firstName()`), or the value is actually static and should be a plain constant, a `signal()`, or an `afterNextRender()` in the case of `afterRenderEffect()`. To avoid false positives, the rule only reports when everything the tracked function does is known not to read a signal. Calls into the TypeScript standard library (arrays, strings, `Math`, `JSON`, `console`, DOM APIs, etc.) are known to be safe; anything else keeps the rule silent, including a helper function, a service method, a getter, an unresolved symbol, and any Angular API such as `untracked()`. One known limitation: a standard library call that reaches user code through a protocol method, such as `toJSON()` in `JSON.stringify()` or `toString()` in a template literal, is still treated as safe, so a signal read inside such a method is not detected. For `resource()`/`rxResource()` only the `params` function defines the dependencies, and these are opt-in via the `checkResources` option.',
|
|
357
|
+
};
|
|
@@ -16,8 +16,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
16
16
|
messages: {
|
|
17
17
|
relativeUrlPrefix: `The ./ and ../ prefix is standard syntax for relative URLs.`,
|
|
18
18
|
},
|
|
19
|
+
defaultOptions: [],
|
|
19
20
|
},
|
|
20
|
-
defaultOptions: [],
|
|
21
21
|
create(context) {
|
|
22
22
|
return {
|
|
23
23
|
[`${utils_1.Selectors.COMPONENT_CLASS_DECORATOR} Property[key.name='templateUrl']`]({ value, }) {
|
|
@@ -25,8 +25,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
25
25
|
messages: {
|
|
26
26
|
defineOnPrototype: `The {{ method }} lifecycle method should be defined on the object's prototype. See more at ${ISSUE_LINK}`,
|
|
27
27
|
},
|
|
28
|
+
defaultOptions: [],
|
|
28
29
|
},
|
|
29
|
-
defaultOptions: [],
|
|
30
30
|
create(context) {
|
|
31
31
|
return {
|
|
32
32
|
[propertyDefinitionSelector](node) {
|
|
@@ -44,8 +44,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
44
44
|
requireLocalizeMeaning: `$localize tagged messages should contain a meaning. See more at ${STYLE_GUIDE_LINK_METADATA_FOR_TRANSLATION}`,
|
|
45
45
|
requireLocalizeCustomId: `$localize tagged messages should contain a custom id{{patternMessage}}. See more at ${STYLE_GUIDE_LINK_METADATA_FOR_TRANSLATION}`,
|
|
46
46
|
},
|
|
47
|
+
defaultOptions: [DEFAULT_OPTIONS],
|
|
47
48
|
},
|
|
48
|
-
defaultOptions: [DEFAULT_OPTIONS],
|
|
49
49
|
create(context, [{ requireDescription, requireMeaning, requireCustomId }]) {
|
|
50
50
|
return {
|
|
51
51
|
TaggedTemplateExpression(taggedTemplateExpression) {
|
|
@@ -15,8 +15,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
15
15
|
messages: {
|
|
16
16
|
runtimeLocalize: `$localize could be called before translations are loaded`,
|
|
17
17
|
},
|
|
18
|
+
defaultOptions: [],
|
|
18
19
|
},
|
|
19
|
-
defaultOptions: [],
|
|
20
20
|
create(context) {
|
|
21
21
|
return {
|
|
22
22
|
TaggedTemplateExpression(taggedTemplateExpression) {
|
|
@@ -103,8 +103,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
103
103
|
messages: {
|
|
104
104
|
incorrectOrder: 'Keys in @{{decorator}} decorator should be ordered: {{expectedOrder}}',
|
|
105
105
|
},
|
|
106
|
+
defaultOptions: [DEFAULT_ORDER],
|
|
106
107
|
},
|
|
107
|
-
defaultOptions: [DEFAULT_ORDER],
|
|
108
108
|
create(context, [orderConfig]) {
|
|
109
109
|
function checkContext(node, decoratorName) {
|
|
110
110
|
const expectedOrder = orderConfig[decoratorName];
|
|
@@ -15,8 +15,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
15
15
|
messages: {
|
|
16
16
|
lifecycleMethodsNotSorted: `Lifecycle Methods are not declared in order of execution`,
|
|
17
17
|
},
|
|
18
|
+
defaultOptions: [],
|
|
18
19
|
},
|
|
19
|
-
defaultOptions: [],
|
|
20
20
|
create(context) {
|
|
21
21
|
const isBefore = (method1, method2) => {
|
|
22
22
|
const methodIndex1 = utils_1.ASTUtils.angularLifecycleMethodsOrdered.indexOf(utils_1.ASTUtils.getMethodName(method1));
|
|
@@ -16,8 +16,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
16
16
|
messages: {
|
|
17
17
|
useComponentSelector: 'The selector of the component is mandatory',
|
|
18
18
|
},
|
|
19
|
+
defaultOptions: [],
|
|
19
20
|
},
|
|
20
|
-
defaultOptions: [],
|
|
21
21
|
create(context) {
|
|
22
22
|
return {
|
|
23
23
|
[utils_1.Selectors.COMPONENT_CLASS_DECORATOR](node) {
|
|
@@ -18,8 +18,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
18
18
|
useComponentViewEncapsulation: `Using \`${VIEW_ENCAPSULATION_NONE}\` makes your styles global, which may have an unintended effect`,
|
|
19
19
|
suggestRemoveViewEncapsulationNone: `Remove \`${VIEW_ENCAPSULATION_NONE}\``,
|
|
20
20
|
},
|
|
21
|
+
defaultOptions: [],
|
|
21
22
|
},
|
|
22
|
-
defaultOptions: [],
|
|
23
23
|
create(context) {
|
|
24
24
|
const sourceCode = context.sourceCode;
|
|
25
25
|
return {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
export type Options = [
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export type Options = [
|
|
2
|
+
{
|
|
3
|
+
readonly ignoreClassNamePattern?: string;
|
|
4
|
+
readonly allowProvidedInNull?: boolean;
|
|
5
|
+
}
|
|
6
|
+
];
|
|
4
7
|
export type MessageIds = 'useInjectableProvidedIn' | 'suggestInjector';
|
|
5
8
|
export declare const RULE_NAME = "use-injectable-provided-in";
|
|
6
9
|
declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<MessageIds, Options, import("../utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-injectable-provided-in.d.ts","sourceRoot":"","sources":["../../src/rules/use-injectable-provided-in.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,OAAO,GAAG
|
|
1
|
+
{"version":3,"file":"use-injectable-provided-in.d.ts","sourceRoot":"","sources":["../../src/rules/use-injectable-provided-in.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;QACzC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;KACxC;CACF,CAAC;AACF,MAAM,MAAM,UAAU,GAAG,yBAAyB,GAAG,iBAAiB,CAAC;AACvE,eAAO,MAAM,SAAS,+BAA+B,CAAC;;;;AAMtD,wBAiEG;AAEH,eAAO,MAAM,mBAAmB;;CAE/B,CAAC"}
|
|
@@ -5,6 +5,9 @@ const utils_1 = require("@angular-eslint/utils");
|
|
|
5
5
|
const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
6
6
|
exports.RULE_NAME = 'use-injectable-provided-in';
|
|
7
7
|
const METADATA_PROPERTY_NAME = 'providedIn';
|
|
8
|
+
const DEFAULT_OPTIONS = {
|
|
9
|
+
allowProvidedInNull: false,
|
|
10
|
+
};
|
|
8
11
|
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
9
12
|
name: exports.RULE_NAME,
|
|
10
13
|
meta: {
|
|
@@ -20,6 +23,10 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
20
23
|
ignoreClassNamePattern: {
|
|
21
24
|
type: 'string',
|
|
22
25
|
},
|
|
26
|
+
allowProvidedInNull: {
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
default: DEFAULT_OPTIONS.allowProvidedInNull,
|
|
29
|
+
},
|
|
23
30
|
},
|
|
24
31
|
additionalProperties: false,
|
|
25
32
|
},
|
|
@@ -28,16 +35,18 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
28
35
|
useInjectableProvidedIn: `The \`${METADATA_PROPERTY_NAME}\` property is mandatory for \`Injectables\``,
|
|
29
36
|
suggestInjector: `Use \`${METADATA_PROPERTY_NAME}: '{{injector}}'\``,
|
|
30
37
|
},
|
|
38
|
+
defaultOptions: [DEFAULT_OPTIONS],
|
|
31
39
|
},
|
|
32
|
-
|
|
33
|
-
create(context, [{ ignoreClassNamePattern }]) {
|
|
40
|
+
create(context, [{ ignoreClassNamePattern, allowProvidedInNull }]) {
|
|
34
41
|
const injectableClassDecorator = `ClassDeclaration:not([id.name=${ignoreClassNamePattern}]):not(:has(TSClassImplements:matches([expression.property.name='HttpInterceptor'], [expression.name='HttpInterceptor']))) > Decorator[expression.callee.name="Injectable"]`;
|
|
35
42
|
const providedInMetadataProperty = utils_1.Selectors.metadataProperty(METADATA_PROPERTY_NAME);
|
|
36
43
|
const withoutProvidedInDecorator = `${injectableClassDecorator}:matches([expression.arguments.length=0], [expression.arguments.0.type='ObjectExpression']:not(:has(${providedInMetadataProperty})))`;
|
|
37
|
-
const
|
|
44
|
+
const undefinedProvidedInProperty = `${injectableClassDecorator} ${providedInMetadataProperty}[value.type='Identifier'][value.name='undefined']`;
|
|
45
|
+
const nullProvidedInProperty = `${injectableClassDecorator} ${providedInMetadataProperty}[value.type='Literal'][value.raw='null']`;
|
|
38
46
|
const selectors = [
|
|
39
47
|
withoutProvidedInDecorator,
|
|
40
|
-
|
|
48
|
+
undefinedProvidedInProperty,
|
|
49
|
+
...(allowProvidedInNull ? [] : [nullProvidedInProperty]),
|
|
41
50
|
].join(',');
|
|
42
51
|
return {
|
|
43
52
|
[selectors](node) {
|
|
@@ -59,5 +68,5 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
59
68
|
},
|
|
60
69
|
});
|
|
61
70
|
exports.RULE_DOCS_EXTENSION = {
|
|
62
|
-
rationale: `Using 'providedIn' in the @Injectable() decorator (like '@Injectable({ providedIn: "root" })') enables tree-shaking, which means Angular can remove the service from your production bundle if it's never actually used. Without 'providedIn', services must be registered in a module's providers array, and Angular must include them in the bundle even if they're unused. This can significantly increase bundle size. Additionally, 'providedIn' makes services easier to use (no need to add them to module providers) and makes circular dependencies less likely. The 'providedIn' syntax is the modern, recommended way to provide services.`,
|
|
71
|
+
rationale: `Using 'providedIn' in the @Injectable() decorator (like '@Injectable({ providedIn: "root" })') enables tree-shaking, which means Angular can remove the service from your production bundle if it's never actually used. Without 'providedIn', services must be registered in a module's providers array, and Angular must include them in the bundle even if they're unused. This can significantly increase bundle size. Additionally, 'providedIn' makes services easier to use (no need to add them to module providers) and makes circular dependencies less likely. The 'providedIn' syntax is the modern, recommended way to provide services. Angular treats 'providedIn: null' as equivalent to omitting the property, so it is flagged by default. Set 'allowProvidedInNull' to true if you need to permit this pattern.`,
|
|
63
72
|
};
|
|
@@ -17,8 +17,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
17
17
|
useLifecycleInterface: `Lifecycle interface '{{interfaceName}}' should be implemented for method '{{methodName}}'. (${STYLE_GUIDE_LINK})`,
|
|
18
18
|
},
|
|
19
19
|
fixable: 'code',
|
|
20
|
+
defaultOptions: [],
|
|
20
21
|
},
|
|
21
|
-
defaultOptions: [],
|
|
22
22
|
create(context) {
|
|
23
23
|
const angularLifecycleMethodsPattern = (0, utils_1.toPattern)([
|
|
24
24
|
...utils_1.ASTUtils.ANGULAR_LIFECYCLE_METHODS,
|
|
@@ -18,8 +18,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
18
18
|
messages: {
|
|
19
19
|
usePipeTransformInterface: `Pipes should implement \`${PIPE_TRANSFORM}\` interface`,
|
|
20
20
|
},
|
|
21
|
+
defaultOptions: [],
|
|
21
22
|
},
|
|
22
|
-
defaultOptions: [],
|
|
23
23
|
create(context) {
|
|
24
24
|
return {
|
|
25
25
|
[`ClassDeclaration:not(:has(TSClassImplements:matches([expression.name='${PIPE_TRANSFORM}'], [expression.property.name='${PIPE_TRANSFORM}']))) > Decorator[expression.callee.name='Pipe']`]({ parent: classDeclaration, }) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-eslint-rule.d.ts","sourceRoot":"","sources":["../../src/utils/create-eslint-rule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD,MAAM,WAAW,QAAQ;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;
|
|
1
|
+
{"version":3,"file":"create-eslint-rule.d.ts","sourceRoot":"","sources":["../../src/utils/create-eslint-rule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD,MAAM,WAAW,QAAQ;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,gBAAgB;;CAG5B,CAAC"}
|
|
@@ -2,27 +2,4 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createESLintRule = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
-
|
|
6
|
-
* We need to patch the RuleCreator in order to preserve the defaultOptions
|
|
7
|
-
* to use as part of documentation generation.
|
|
8
|
-
*/
|
|
9
|
-
const patchedRuleCreator = (urlCreator) => {
|
|
10
|
-
return function createRule({ name, meta, defaultOptions, create }) {
|
|
11
|
-
const resolvedDefaultOptions = (defaultOptions ?? []);
|
|
12
|
-
return {
|
|
13
|
-
name,
|
|
14
|
-
meta: Object.assign(Object.assign({}, meta), {
|
|
15
|
-
docs: Object.assign(Object.assign({}, meta.docs), {
|
|
16
|
-
url: urlCreator(name),
|
|
17
|
-
}),
|
|
18
|
-
}),
|
|
19
|
-
defaultOptions: resolvedDefaultOptions,
|
|
20
|
-
create(context) {
|
|
21
|
-
const optionsWithDefault = utils_1.ESLintUtils.applyDefault(resolvedDefaultOptions, context.options);
|
|
22
|
-
return create(context, optionsWithDefault);
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
};
|
|
26
|
-
};
|
|
27
|
-
patchedRuleCreator.withoutDocs = utils_1.ESLintUtils.RuleCreator.withoutDocs;
|
|
28
|
-
exports.createESLintRule = patchedRuleCreator((ruleName) => `https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/${ruleName}.md`);
|
|
5
|
+
exports.createESLintRule = utils_1.ESLintUtils.RuleCreator((ruleName) => `https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/${ruleName}.md`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@depup/angular-eslint__eslint-plugin",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.3.0-depup.0",
|
|
4
4
|
"description": "ESLint plugin for Angular applications, following https://angular.dev/style-guide (with updated dependencies)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
"changes.json"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"
|
|
24
|
-
"@angular-eslint/
|
|
25
|
-
"
|
|
23
|
+
"@angular-eslint/bundled-angular-compiler": "22.3.0",
|
|
24
|
+
"@angular-eslint/utils": "22.3.0",
|
|
25
|
+
"ts-api-utils": "^2.5.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@angular-eslint/test-utils": "22.
|
|
28
|
+
"@angular-eslint/test-utils": "22.3.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@typescript-eslint/utils": "^8.0.0",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
},
|
|
51
51
|
"depsUpdated": 1,
|
|
52
52
|
"originalPackage": "@angular-eslint/eslint-plugin",
|
|
53
|
-
"originalVersion": "22.
|
|
54
|
-
"processedAt": "2026-
|
|
53
|
+
"originalVersion": "22.3.0",
|
|
54
|
+
"processedAt": "2026-09-07T08:13:08.078Z",
|
|
55
55
|
"smokeTest": "passed"
|
|
56
56
|
}
|
|
57
57
|
}
|