@ethlete/eslint-plugin 0.1.0-next.2 → 0.1.0-next.3

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.
@@ -0,0 +1,88 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ /** @type {import('eslint').Rule.RuleModule} */
5
+ const noTypedInjectedElementRef = {
6
+ meta: {
7
+ type: 'suggestion',
8
+ docs: {
9
+ description:
10
+ 'Require that inject(ElementRef) uses a generic type parameter on inject() itself, not on ElementRef.',
11
+ recommended: true,
12
+ },
13
+ messages: {
14
+ missingGeneric:
15
+ 'Provide a generic type on inject() itself: `inject<ElementRef<HTMLElement>>(ElementRef)`. Passing a type argument on ElementRef directly is also disallowed.',
16
+ },
17
+ schema: [],
18
+ fixable: 'code',
19
+ },
20
+ create(context) {
21
+ return {
22
+ CallExpression(node) {
23
+ if (node.callee.type !== 'Identifier' || node.callee.name !== 'inject') {
24
+ return;
25
+ }
26
+
27
+ const firstArg = node.arguments[0];
28
+ if (!firstArg) return;
29
+
30
+ const isPlainElementRef = firstArg.type === 'Identifier' && firstArg.name === 'ElementRef';
31
+ const isInstantiatedElementRef =
32
+ firstArg.type === 'TSInstantiationExpression' &&
33
+ firstArg.expression &&
34
+ firstArg.expression.type === 'Identifier' &&
35
+ firstArg.expression.name === 'ElementRef';
36
+
37
+ if (!isPlainElementRef && !isInstantiatedElementRef) return;
38
+
39
+ const src = context.sourceCode;
40
+ const existingTypeParams = node.typeParameters || node.typeArguments;
41
+ const hasInjectTypeParam =
42
+ existingTypeParams && existingTypeParams.params && existingTypeParams.params.length > 0;
43
+
44
+ if (isPlainElementRef && !hasInjectTypeParam) {
45
+ // inject(ElementRef) → inject<ElementRef<HTMLElement>>(ElementRef)
46
+ context.report({
47
+ node,
48
+ messageId: 'missingGeneric',
49
+ fix(fixer) {
50
+ return fixer.insertTextAfter(node.callee, '<ElementRef<HTMLElement>>');
51
+ },
52
+ });
53
+ return;
54
+ }
55
+
56
+ if (isInstantiatedElementRef) {
57
+ // Capture the type args text from the token, e.g. "<HTMLElement>"
58
+ const tokenTypeParamText = src.getText(firstArg.typeParameters || firstArg.typeArguments);
59
+
60
+ if (!hasInjectTypeParam) {
61
+ // inject(ElementRef<HTMLElement>) → inject<ElementRef<HTMLElement>>(ElementRef)
62
+ context.report({
63
+ node,
64
+ messageId: 'missingGeneric',
65
+ fix(fixer) {
66
+ return [
67
+ fixer.insertTextAfter(node.callee, `<ElementRef${tokenTypeParamText}>`),
68
+ fixer.replaceText(firstArg, 'ElementRef'),
69
+ ];
70
+ },
71
+ });
72
+ } else {
73
+ // inject<X>(ElementRef<Y>) — token must not carry type args; keep inject's type param
74
+ context.report({
75
+ node,
76
+ messageId: 'missingGeneric',
77
+ fix(fixer) {
78
+ return fixer.replaceText(firstArg, 'ElementRef');
79
+ },
80
+ });
81
+ }
82
+ }
83
+ },
84
+ };
85
+ },
86
+ };
87
+
88
+ module.exports = noTypedInjectedElementRef;
@@ -0,0 +1,190 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ const {
5
+ getAngularMetadata,
6
+ getMemberName,
7
+ isReferencedFromTemplateOrHost,
8
+ } = require('./internals/angular-member-visibility');
9
+ const { getImplementedContractMemberNames } = require('./internals/implemented-contract-members');
10
+
11
+ /**
12
+ * @param {any} node
13
+ */
14
+ const isInjectCall = (node) => {
15
+ if (!node || node.type !== 'PropertyDefinition' || !node.value) return false;
16
+ if (node.value.type !== 'CallExpression') return false;
17
+ return node.value.callee.type === 'Identifier' && node.value.callee.name === 'inject';
18
+ };
19
+
20
+ /**
21
+ * @param {any} node
22
+ * @param {import('eslint').SourceCode} sourceCode
23
+ */
24
+ const buildPublicFix = (node, sourceCode) => {
25
+ const keyToken = sourceCode.getFirstToken(node.key);
26
+ if (!keyToken) return null;
27
+
28
+ if (node.accessibility) {
29
+ const firstToken = sourceCode.getFirstToken(node);
30
+ if (!firstToken) return null;
31
+
32
+ return (fixer) => fixer.replaceText(firstToken, 'public');
33
+ }
34
+
35
+ return (fixer) => fixer.insertTextBefore(keyToken, 'public ');
36
+ };
37
+
38
+ /**
39
+ * @param {any} node
40
+ * @param {import('eslint').SourceCode} sourceCode
41
+ */
42
+ const buildRemoveProtectedFix = (node, sourceCode) => {
43
+ const protectedToken = sourceCode.getFirstToken(node, (token) => token.value === 'protected');
44
+ const nextToken = protectedToken ? sourceCode.getTokenAfter(protectedToken) : null;
45
+
46
+ if (!protectedToken || !nextToken) {
47
+ return null;
48
+ }
49
+
50
+ return (fixer) => fixer.removeRange([protectedToken.range[0], nextToken.range[0]]);
51
+ };
52
+
53
+ /**
54
+ * @param {any} node
55
+ */
56
+ const isSupportedMember = (node) => {
57
+ if (node.type === 'PropertyDefinition') {
58
+ return true;
59
+ }
60
+
61
+ if (node.type === 'MethodDefinition') {
62
+ return node.kind !== 'constructor';
63
+ }
64
+
65
+ return false;
66
+ };
67
+
68
+ /** @type {import('eslint').Rule.RuleModule} */
69
+ const templateMemberAccessibility = {
70
+ meta: {
71
+ type: 'problem',
72
+ docs: {
73
+ description:
74
+ 'Require non-injected Angular class members referenced from templates or host bindings to use an explicit accessibility modifier, require explicit public on implicitly public surface members, and disallow unnecessary protected on other members.',
75
+ recommended: true,
76
+ },
77
+ fixable: 'code',
78
+ schema: [],
79
+ messages: {
80
+ shouldBePublic: 'Member `{{name}}` implements a public contract and should use an explicit `public` modifier.',
81
+ shouldBeExplicitPublic:
82
+ 'Member `{{name}}` is implicitly public. Use an explicit `public` modifier. Keep or add `@internal` when it is only technically reachable.',
83
+ shouldBeExplicit:
84
+ 'Member `{{name}}` is referenced from a template or host binding. Use `public` when it is intentionally exposed outside the component; otherwise use `protected`.',
85
+ shouldNotBeProtected:
86
+ 'Member `{{name}}` should not be protected because it is not referenced from a template or host binding.',
87
+ },
88
+ },
89
+ create(context) {
90
+ const sourceCode = context.sourceCode;
91
+
92
+ /**
93
+ * @param {any} node
94
+ */
95
+ const checkMember = (node) => {
96
+ if (!isSupportedMember(node)) return;
97
+ if (node.type === 'PropertyDefinition' && isInjectCall(node)) return;
98
+
99
+ const classNode = node.parent && node.parent.type === 'ClassBody' ? node.parent.parent : null;
100
+ if (!classNode || (classNode.type !== 'ClassDeclaration' && classNode.type !== 'ClassExpression')) return;
101
+ if (!getAngularMetadata(classNode)) return;
102
+
103
+ const memberName = getMemberName(node);
104
+ if (!memberName) return;
105
+
106
+ const implementedContractMembers = getImplementedContractMemberNames({ classNode, context });
107
+ const implementsPublicContract = implementedContractMembers.has(memberName);
108
+
109
+ if (implementsPublicContract) {
110
+ if (node.accessibility === 'public') return;
111
+
112
+ context.report({
113
+ node,
114
+ messageId: 'shouldBePublic',
115
+ data: { name: memberName },
116
+ fix: buildPublicFix(node, sourceCode),
117
+ });
118
+
119
+ return;
120
+ }
121
+
122
+ const isTemplateVisible = isReferencedFromTemplateOrHost(classNode, memberName, context);
123
+
124
+ if (isTemplateVisible) {
125
+ if (node.accessibility === 'protected' || node.accessibility === 'public') return;
126
+
127
+ context.report({
128
+ node,
129
+ messageId: 'shouldBeExplicit',
130
+ data: { name: memberName },
131
+ fix: buildPublicFix(node, sourceCode),
132
+ });
133
+
134
+ return;
135
+ }
136
+
137
+ if (node.accessibility !== 'protected') return;
138
+
139
+ context.report({
140
+ node,
141
+ messageId: 'shouldNotBeProtected',
142
+ data: { name: memberName },
143
+ fix: buildRemoveProtectedFix(node, sourceCode),
144
+ });
145
+
146
+ return;
147
+ };
148
+
149
+ /**
150
+ * @param {any} node
151
+ */
152
+ const checkImplicitPublicMember = (node) => {
153
+ if (!isSupportedMember(node)) return;
154
+ if (node.type === 'PropertyDefinition' && isInjectCall(node)) return;
155
+
156
+ const classNode = node.parent && node.parent.type === 'ClassBody' ? node.parent.parent : null;
157
+ if (!classNode || (classNode.type !== 'ClassDeclaration' && classNode.type !== 'ClassExpression')) return;
158
+ if (!getAngularMetadata(classNode)) return;
159
+
160
+ const memberName = getMemberName(node);
161
+ if (!memberName) return;
162
+
163
+ const implementedContractMembers = getImplementedContractMemberNames({ classNode, context });
164
+ if (implementedContractMembers.has(memberName)) return;
165
+
166
+ if (isReferencedFromTemplateOrHost(classNode, memberName, context)) return;
167
+ if (node.accessibility) return;
168
+
169
+ context.report({
170
+ node,
171
+ messageId: 'shouldBeExplicitPublic',
172
+ data: { name: memberName },
173
+ fix: buildPublicFix(node, sourceCode),
174
+ });
175
+ };
176
+
177
+ return {
178
+ PropertyDefinition(node) {
179
+ checkMember(node);
180
+ checkImplicitPublicMember(node);
181
+ },
182
+ MethodDefinition(node) {
183
+ checkMember(node);
184
+ checkImplicitPublicMember(node);
185
+ },
186
+ };
187
+ },
188
+ };
189
+
190
+ module.exports = templateMemberAccessibility;