@ethlete/eslint-plugin 1.0.0-next.16 → 1.0.0-next.17
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/CHANGELOG.md +18 -9
- package/package.json +1 -1
- package/src/configs/recommended.js +4 -2
- package/src/index.js +2 -0
- package/src/rules/no-impure-top-level-provider.js +129 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0-next.17
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#3041](https://github.com/ethlete-io/ethdk/pull/3041) [`ae18442`](https://github.com/ethlete-io/ethdk/commit/ae18442bcbeaf0bc1b023812f4e507169db56c88) Thanks [@github-actions](https://github.com/apps/github-actions)! - New rule `no-impure-top-level-provider`: bans destructuring a factory call at module scope, and - with
|
|
8
|
+
`{ requirePureAnnotation: true }`, for publishable library source - requires `/* @__PURE__ */` on
|
|
9
|
+
module-scope calls. Both shapes are undroppable, so one of them puts everything the call touches into
|
|
10
|
+
every consumer's bundle. Fixable.
|
|
11
|
+
|
|
3
12
|
## 1.0.0-next.16
|
|
4
13
|
|
|
5
14
|
### Minor Changes
|
|
6
15
|
|
|
7
16
|
- [#3037](https://github.com/ethlete-io/ethdk/pull/3037) [`19469c2`](https://github.com/ethlete-io/ethdk/commit/19469c21903217e05b974ac69773f222e63ae4e1) Thanks [@github-actions](https://github.com/apps/github-actions)! - Add `no-effect-cleanup-return`: flags a cleanup function returned from `effect()` /
|
|
8
|
-
`afterRenderEffect()`, which Angular ignores
|
|
17
|
+
`afterRenderEffect()`, which Angular ignores - so the teardown silently never runs. Auto-fixes the
|
|
9
18
|
mechanical case to the `onCleanup` parameter; otherwise points at `inject(DestroyRef).onDestroy()`.
|
|
10
19
|
|
|
11
20
|
- [#3037](https://github.com/ethlete-io/ethdk/pull/3037) [`3151b7a`](https://github.com/ethlete-io/ethdk/commit/3151b7a253d14e38e22e20d67bf0191f141c144e) Thanks [@github-actions](https://github.com/apps/github-actions)! - Add `ethlete/no-template-literal-before-inline-template`, and restructure the files it flagged.
|
|
12
21
|
|
|
13
|
-
The Angular VS Code extension decides **client-side** whether the cursor sits inside an inline `template:` before it forwards completion, hover, go-to-definition or signature-help to the language server. That check (`isNotTypescriptOrSupportedDecoratorField`) walks the file with a bare `ts.createScanner()` loop, which cannot re-scan `}` as `TemplateMiddle`/`TemplateTail`
|
|
22
|
+
The Angular VS Code extension decides **client-side** whether the cursor sits inside an inline `template:` before it forwards completion, hover, go-to-definition or signature-help to the language server. That check (`isNotTypescriptOrSupportedDecoratorField`) walks the file with a bare `ts.createScanner()` loop, which cannot re-scan `}` as `TemplateMiddle`/`TemplateTail` - that needs the parser's `reScanTemplateToken()`. So the first template literal containing a `${…}` substitution desynchronises both the token stream and the brace counter, the scanner never recognises `template` `:` again, and every template request below it is dropped. The language server answers those requests correctly; the editor just never asks, so the template silently has no IntelliSense at all.
|
|
14
23
|
|
|
15
|
-
The new rule reproduces that scanner verbatim, so it reports exactly the templates the extension would abandon
|
|
24
|
+
The new rule reproduces that scanner verbatim, so it reports exactly the templates the extension would abandon - no heuristic. Twenty inline templates across `components`, `cdk` and the playground were affected, all of them behind a fixture or helper that happened to use an interpolated template literal. Story fixtures moved into sibling `*-storybook.data.ts` files; spec fixtures and in-class helpers that must stay above their component (because a later `@Component` references the class in `imports`) were rewritten without the interpolation.
|
|
16
25
|
|
|
17
|
-
No public API changed
|
|
26
|
+
No public API changed - the `components` and `cdk` bumps are story/spec restructuring plus moving `signalVisibilityChangeClasses` below `RichFilterHostComponent` in the same module.
|
|
18
27
|
|
|
19
28
|
## 1.0.0-next.15
|
|
20
29
|
|
|
@@ -28,13 +37,13 @@
|
|
|
28
37
|
|
|
29
38
|
- [`423b30b`](https://github.com/ethlete-io/ethdk/commit/423b30ba4f368d7cee8c464ebd89b5df20d3934c) Thanks [@TomTomB](https://github.com/TomTomB)! - The `static` class member ban now excepts `ngTemplateContextGuard`, which Angular's template type checker requires to be static.
|
|
30
39
|
|
|
31
|
-
- [`7ca77d4`](https://github.com/ethlete-io/ethdk/commit/7ca77d4a9e1dd3abad7237227c62643159719b74) Thanks [@TomTomB](https://github.com/TomTomB)! - `no-trivial-return-type`: self-referencing (recursive) functions keep their return type annotation
|
|
40
|
+
- [`7ca77d4`](https://github.com/ethlete-io/ethdk/commit/7ca77d4a9e1dd3abad7237227c62643159719b74) Thanks [@TomTomB](https://github.com/TomTomB)! - `no-trivial-return-type`: self-referencing (recursive) functions keep their return type annotation - TypeScript cannot infer a return type that depends on itself (TS7023), so the fixer no longer strips it there.
|
|
32
41
|
|
|
33
42
|
## 1.0.0-next.13
|
|
34
43
|
|
|
35
44
|
### Minor Changes
|
|
36
45
|
|
|
37
|
-
- [#3022](https://github.com/ethlete-io/ethdk/pull/3022) [`5a43c83`](https://github.com/ethlete-io/ethdk/commit/5a43c8347a98a815562d1bb0f59bc0db1d765262) Thanks [@github-actions](https://github.com/apps/github-actions)! - Recommended config: the `@Injectable` ban now also flags Angular 22's `@Service` decorator
|
|
46
|
+
- [#3022](https://github.com/ethlete-io/ethdk/pull/3022) [`5a43c83`](https://github.com/ethlete-io/ethdk/commit/5a43c8347a98a815562d1bb0f59bc0db1d765262) Thanks [@github-actions](https://github.com/apps/github-actions)! - Recommended config: the `@Injectable` ban now also flags Angular 22's `@Service` decorator - use `createProvider` / `createRootProvider` from `@ethlete/core` instead.
|
|
38
47
|
|
|
39
48
|
- [#3022](https://github.com/ethlete-io/ethdk/pull/3022) [`9be3738`](https://github.com/ethlete-io/ethdk/commit/9be3738e18ef1837841c79131127fa42406a3e92) Thanks [@github-actions](https://github.com/apps/github-actions)! - New template rule `ethlete/prefer-static-boolean-properties` (in `recommendedTemplate` as `warn`): flags property bindings of static booleans like `[isReadonly]="true"` and suggests the static-attribute form (`isReadonly` / `isReadonly="false"`). Suggestion-only, since the rewrite is only safe for inputs with a `booleanAttribute` transform.
|
|
40
49
|
|
|
@@ -43,8 +52,8 @@
|
|
|
43
52
|
### Minor Changes
|
|
44
53
|
|
|
45
54
|
- [`44adcac`](https://github.com/ethlete-io/ethdk/commit/44adcac94d7e0f56742e02901221c6e04da7df47) Thanks [@TomTomB](https://github.com/TomTomB)! - Add two component I/O naming rules:
|
|
46
|
-
- `no-native-html-input-name` (error)
|
|
47
|
-
- `prefer-present-tense-output` (warn)
|
|
55
|
+
- `no-native-html-input-name` (error) - flags an `input()`/`model()` named after a global HTML attribute (`title`, `id`, `hidden`, `role`, `tabindex`, …), which collides with the attribute the host element carries natively.
|
|
56
|
+
- `prefer-present-tense-output` (warn) - nudges `output()` names toward the present tense like native DOM events (`playerSelect`, not `playerSelected`).
|
|
48
57
|
|
|
49
58
|
The `on`-prefix case is already covered by `@angular-eslint/no-output-on-prefix`, so no rule is added for it.
|
|
50
59
|
|
|
@@ -52,7 +61,7 @@
|
|
|
52
61
|
|
|
53
62
|
### Major Changes
|
|
54
63
|
|
|
55
|
-
- [#3016](https://github.com/ethlete-io/ethdk/pull/3016) [`396bdfb`](https://github.com/ethlete-io/ethdk/commit/396bdfb50dc51d2de0156dd7b7cc0ae3b21dfe9b) Thanks [@github-actions](https://github.com/apps/github-actions)! - Remove the `no-public-property` rule. It contradicted `template-member-accessibility`, which requires an explicit `public` modifier on surface members
|
|
64
|
+
- [#3016](https://github.com/ethlete-io/ethdk/pull/3016) [`396bdfb`](https://github.com/ethlete-io/ethdk/commit/396bdfb50dc51d2de0156dd7b7cc0ae3b21dfe9b) Thanks [@github-actions](https://github.com/apps/github-actions)! - Remove the `no-public-property` rule. It contradicted `template-member-accessibility`, which requires an explicit `public` modifier on surface members - the explicit-public style is the intended one. The rule was never part of the `recommended` config; if you enabled `ethlete/no-public-property` manually, drop it from your ESLint config.
|
|
56
65
|
|
|
57
66
|
## 0.1.0-next.10
|
|
58
67
|
|
package/package.json
CHANGED
|
@@ -170,12 +170,12 @@ const recommendedTs = {
|
|
|
170
170
|
message:
|
|
171
171
|
'No legacy Angular lifecycle hooks. Use afterNextRender, inject(DestroyRef).onDestroy, or constructor-based effects instead.',
|
|
172
172
|
},
|
|
173
|
-
// No @Injectable / @Service — use
|
|
173
|
+
// No @Injectable / @Service — use defineRootProvider / defineProvider from @ethlete/core
|
|
174
174
|
{
|
|
175
175
|
selector:
|
|
176
176
|
'Decorator[expression.callee.name=/^(Injectable|Service)$/], Decorator[expression.name=/^(Injectable|Service)$/]',
|
|
177
177
|
message:
|
|
178
|
-
'No @Injectable / @Service services. Use
|
|
178
|
+
'No @Injectable / @Service services. Use defineRootProvider / defineProvider from @ethlete/core instead.',
|
|
179
179
|
},
|
|
180
180
|
// No route guards — handle access control inside the component itself
|
|
181
181
|
{
|
|
@@ -295,6 +295,8 @@ const recommendedTs = {
|
|
|
295
295
|
// No empty blank lines between import declarations
|
|
296
296
|
'ethlete/no-empty-newlines-between-imports': 'error',
|
|
297
297
|
|
|
298
|
+
'ethlete/no-impure-top-level-provider': 'error',
|
|
299
|
+
|
|
298
300
|
// No legacy Angular decorators — use signal-based APIs (input, output, viewChild, etc.)
|
|
299
301
|
// and host: {} bindings (instead of @HostBinding / @HostListener)
|
|
300
302
|
'ethlete/no-legacy-angular-decorators': 'error',
|
package/src/index.js
CHANGED
|
@@ -53,6 +53,7 @@ const noTypedInjectedElementRef = require('./rules/no-typed-injected-element-ref
|
|
|
53
53
|
const noNativeHtmlInputName = require('./rules/no-native-html-input-name');
|
|
54
54
|
const preferPresentTenseOutput = require('./rules/prefer-present-tense-output');
|
|
55
55
|
const preferStaticBooleanProperties = require('./rules/prefer-static-boolean-properties');
|
|
56
|
+
const noImpureTopLevelProvider = require('./rules/no-impure-top-level-provider');
|
|
56
57
|
const { recommendedTs, recommendedTemplate, recommendedSpec } = require('./configs/recommended');
|
|
57
58
|
|
|
58
59
|
/** @type {import('eslint').ESLint.Plugin} */
|
|
@@ -114,6 +115,7 @@ const plugin = {
|
|
|
114
115
|
'no-native-html-input-name': noNativeHtmlInputName,
|
|
115
116
|
'prefer-present-tense-output': preferPresentTenseOutput,
|
|
116
117
|
'prefer-static-boolean-properties': preferStaticBooleanProperties,
|
|
118
|
+
'no-impure-top-level-provider': noImpureTopLevelProvider,
|
|
117
119
|
},
|
|
118
120
|
};
|
|
119
121
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const PURE_ANNOTATION = /^\s*[#@]__PURE__\s*$/;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Angular's own app build already pure-annotates these at module scope
|
|
8
|
+
* (`pure-toplevel-functions`'s `sideEffectFreeConstructors`), so requiring it in source is noise.
|
|
9
|
+
*/
|
|
10
|
+
const AUTO_ANNOTATED_CONSTRUCTORS = new Set(['InjectionToken']);
|
|
11
|
+
|
|
12
|
+
/** Nothing inside a function body runs on import, so it needs no annotation. */
|
|
13
|
+
const NOT_EVALUATED_ON_IMPORT = new Set(['ArrowFunctionExpression', 'FunctionExpression', 'ClassExpression']);
|
|
14
|
+
|
|
15
|
+
const isModuleScope = (declarator) => {
|
|
16
|
+
const declaration = declarator.parent;
|
|
17
|
+
const container = declaration?.parent;
|
|
18
|
+
|
|
19
|
+
if (container?.type === 'Program') return true;
|
|
20
|
+
|
|
21
|
+
return container?.type === 'ExportNamedDeclaration' && container.parent?.type === 'Program';
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const hasPureAnnotation = (sourceCode, node) =>
|
|
25
|
+
sourceCode.getCommentsBefore(node).some((comment) => comment.type === 'Block' && PURE_ANNOTATION.test(comment.value));
|
|
26
|
+
|
|
27
|
+
/** `f()`, `ns.f()` and `new X()` are nameable; an IIFE is not, and is left alone. */
|
|
28
|
+
const calleeName = (node) => {
|
|
29
|
+
const callee = node.callee;
|
|
30
|
+
|
|
31
|
+
if (callee.type === 'Identifier') return callee.name;
|
|
32
|
+
if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') return callee.property.name;
|
|
33
|
+
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Every call/`new` in an initializer that really is evaluated when the module is imported. */
|
|
38
|
+
const collectImportTimeCalls = (root) => {
|
|
39
|
+
const found = [];
|
|
40
|
+
|
|
41
|
+
const visit = (node) => {
|
|
42
|
+
if (!node || typeof node.type !== 'string' || NOT_EVALUATED_ON_IMPORT.has(node.type)) return;
|
|
43
|
+
|
|
44
|
+
if (node.type === 'CallExpression' || node.type === 'NewExpression') found.push(node);
|
|
45
|
+
|
|
46
|
+
for (const key of Object.keys(node)) {
|
|
47
|
+
if (key === 'parent') continue;
|
|
48
|
+
|
|
49
|
+
const child = node[key];
|
|
50
|
+
|
|
51
|
+
if (Array.isArray(child)) child.forEach(visit);
|
|
52
|
+
else if (child && typeof child === 'object') visit(child);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
visit(root);
|
|
57
|
+
|
|
58
|
+
return found;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
62
|
+
const noImpureTopLevelProvider = {
|
|
63
|
+
meta: {
|
|
64
|
+
type: 'problem',
|
|
65
|
+
docs: {
|
|
66
|
+
description:
|
|
67
|
+
'Disallow module-scope destructuring of a factory call, and require a pure annotation on module-scope calls in library source.',
|
|
68
|
+
},
|
|
69
|
+
fixable: 'code',
|
|
70
|
+
schema: [
|
|
71
|
+
{
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
requirePureAnnotation: {
|
|
75
|
+
type: 'boolean',
|
|
76
|
+
description:
|
|
77
|
+
'Also require `@__PURE__` on every call evaluated at module scope. Turn this on in publishable library source; it is pointless in application code, where every statement is retained anyway.',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
additionalProperties: false,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
messages: {
|
|
84
|
+
noDestructuring:
|
|
85
|
+
'Do not destructure a factory call at module scope. Destructuring invokes the iterator protocol, so no bundler can drop the statement — everything the factory closes over ships to every consumer. Assign the result to one `const` and name each binding with its own annotated extractor (`toProvideFn` / `toInjectFn` / `toToken`).',
|
|
86
|
+
missingPure:
|
|
87
|
+
'This module-scope call to `{{name}}` needs a `/* @__PURE__ */` annotation, or the whole declaration is retained in every consumer bundle — one unannotated call anywhere in an initializer (including inside an argument or an object literal) is enough. If the call is genuinely not side-effect free at import time, move it inside a function instead of annotating it: library code must not do work on import.',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
create(context) {
|
|
91
|
+
const sourceCode = context.sourceCode;
|
|
92
|
+
const requirePureAnnotation = context.options[0]?.requirePureAnnotation === true;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
VariableDeclarator(node) {
|
|
96
|
+
if (!isModuleScope(node) || !node.init) return;
|
|
97
|
+
|
|
98
|
+
const initializer = node.init.type === 'TSAsExpression' ? node.init.expression : node.init;
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
initializer.type === 'CallExpression' &&
|
|
102
|
+
(node.id.type === 'ArrayPattern' || node.id.type === 'ObjectPattern')
|
|
103
|
+
) {
|
|
104
|
+
context.report({ node, messageId: 'noDestructuring' });
|
|
105
|
+
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!requirePureAnnotation || node.id.type !== 'Identifier') return;
|
|
110
|
+
|
|
111
|
+
for (const call of collectImportTimeCalls(initializer)) {
|
|
112
|
+
const name = calleeName(call);
|
|
113
|
+
|
|
114
|
+
if (name === null || hasPureAnnotation(sourceCode, call)) continue;
|
|
115
|
+
if (call.type === 'NewExpression' && AUTO_ANNOTATED_CONSTRUCTORS.has(name)) continue;
|
|
116
|
+
|
|
117
|
+
context.report({
|
|
118
|
+
node: call,
|
|
119
|
+
messageId: 'missingPure',
|
|
120
|
+
data: { name },
|
|
121
|
+
fix: (fixer) => fixer.insertTextBefore(call, '/* @__PURE__ */ '),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
module.exports = noImpureTopLevelProvider;
|