@craft-ng/dev-tools 0.5.0-beta.3 → 0.5.0-beta.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-ng/dev-tools",
3
- "version": "0.5.0-beta.3",
3
+ "version": "0.5.0-beta.4",
4
4
  "description": "Development tools for ng-craft: ESLint configs, ESLint rules, and codemods",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -30,8 +30,8 @@ module.exports = {
30
30
  }
31
31
 
32
32
  const text = sourceCode.getText();
33
- // Cheap pre-filter: both the trigger and the delegated outcome must appear.
34
- if (!text.includes('handleExceptions') || !text.includes('globalError')) {
33
+ // Cheap pre-filter: the delegated outcome must appear.
34
+ if (!text.includes('globalError')) {
35
35
  return;
36
36
  }
37
37
 
@@ -208,8 +208,8 @@ function resolveCollectionVar(craftRoutesCall) {
208
208
  return undefined;
209
209
  }
210
210
 
211
- // A route array element is either `route('path', { … })` (optionally followed by
212
- // `.withProviders(…)`) or a plain `{ path: '…', handleExceptions: { … } }`.
211
+ // A route array element is either `route('path', { … }, { … })` (optionally
212
+ // followed by `.withProviders(…)`) or a plain `{ path: '…', handleExceptions: { … } }`.
213
213
  function extractRouteDefinition(element) {
214
214
  if (Node.isObjectLiteralExpression(element)) {
215
215
  const pathValue = readStringProperty(element, 'path');
@@ -225,23 +225,19 @@ function extractRouteDefinition(element) {
225
225
  if (!routeCall) {
226
226
  return undefined;
227
227
  }
228
- const [pathArg, defArg] = routeCall.getArguments();
228
+ const [pathArg, , handlersArg] = routeCall.getArguments();
229
229
  if (
230
230
  !pathArg ||
231
- !defArg ||
232
- !Node.isObjectLiteralExpression(defArg) ||
231
+ !handlersArg ||
232
+ !Node.isObjectLiteralExpression(handlersArg) ||
233
233
  (!Node.isStringLiteral(pathArg) &&
234
234
  !Node.isNoSubstitutionTemplateLiteral(pathArg))
235
235
  ) {
236
236
  return undefined;
237
237
  }
238
- const handleExceptions = readObjectProperty(defArg, 'handleExceptions');
239
- if (!handleExceptions) {
240
- return undefined;
241
- }
242
238
  return {
243
239
  path: pathArg.getLiteralText(),
244
- handleExceptions,
240
+ handleExceptions: handlersArg,
245
241
  reportNode: routeCall,
246
242
  };
247
243
  }
@@ -5,10 +5,14 @@ module.exports = {
5
5
  description:
6
6
  'Disallow Angular inject() usage so dependencies go through craftService or toCraftService.',
7
7
  },
8
+ hasSuggestions: true,
8
9
  schema: [],
9
10
  },
10
11
  create(context) {
11
12
  const angularNamespaceImports = new Set();
13
+ const angularInjectImports = new Map();
14
+ const reportedInjectImports = new Set();
15
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
12
16
 
13
17
  return {
14
18
  ImportDeclaration(node) {
@@ -22,11 +26,7 @@ module.exports = {
22
26
  specifier.imported.type === 'Identifier' &&
23
27
  specifier.imported.name === 'inject'
24
28
  ) {
25
- context.report({
26
- node: specifier,
27
- message:
28
- 'Angular inject() is forbidden. Expose a craftService/toCraftService injector instead.',
29
- });
29
+ angularInjectImports.set(specifier.local.name, specifier);
30
30
  continue;
31
31
  }
32
32
 
@@ -36,24 +36,50 @@ module.exports = {
36
36
  }
37
37
  },
38
38
  CallExpression(node) {
39
- const callee = node.callee;
40
- if (
41
- callee.type !== 'MemberExpression' ||
42
- callee.computed ||
43
- callee.object.type !== 'Identifier' ||
44
- !angularNamespaceImports.has(callee.object.name) ||
45
- callee.property.type !== 'Identifier' ||
46
- callee.property.name !== 'inject'
47
- ) {
39
+ const angularInjectNode = getAngularInjectCallee(
40
+ node.callee,
41
+ angularInjectImports,
42
+ angularNamespaceImports,
43
+ );
44
+ if (!angularInjectNode) {
48
45
  return;
49
46
  }
50
47
 
48
+ if (node.callee.type === 'Identifier') {
49
+ reportedInjectImports.add(node.callee.name);
50
+ }
51
+
52
+ const tokenText = getInjectTokenText(node.arguments[0], sourceCode);
53
+ const helperName = getRecommendedInjectorName(node.arguments[0]);
54
+
51
55
  context.report({
52
- node: callee.property,
53
- message:
54
- 'Angular inject() is forbidden. Expose a craftService/toCraftService injector instead.',
56
+ node: angularInjectNode,
57
+ message: createInjectMessage(tokenText, helperName),
58
+ suggest: [
59
+ createTemporaryDisableSuggestion(
60
+ sourceCode,
61
+ angularInjectNode,
62
+ 'no-angular-inject',
63
+ helperName
64
+ ? `replace this Angular inject(${tokenText}) call with ${helperName} from a craftService/toCraftService adapter`
65
+ : 'replace this Angular inject() call with a craftService/toCraftService injector',
66
+ ),
67
+ ],
55
68
  });
56
69
  },
70
+ 'Program:exit'() {
71
+ for (const [localName, specifier] of angularInjectImports) {
72
+ if (reportedInjectImports.has(localName)) {
73
+ continue;
74
+ }
75
+
76
+ context.report({
77
+ node: specifier,
78
+ message:
79
+ 'Angular inject() is forbidden. Import and use the injectX helper exposed by a craftService/toCraftService adapter instead.',
80
+ });
81
+ }
82
+ },
57
83
  };
58
84
  },
59
85
  };
@@ -61,3 +87,101 @@ module.exports = {
61
87
  function isAngularModule(sourceValue) {
62
88
  return typeof sourceValue === 'string' && sourceValue.startsWith('@angular/');
63
89
  }
90
+
91
+ function getAngularInjectCallee(
92
+ callee,
93
+ angularInjectImports,
94
+ angularNamespaceImports,
95
+ ) {
96
+ if (callee.type === 'Identifier' && angularInjectImports.has(callee.name)) {
97
+ return callee;
98
+ }
99
+
100
+ if (
101
+ callee.type === 'MemberExpression' &&
102
+ !callee.computed &&
103
+ callee.object.type === 'Identifier' &&
104
+ angularNamespaceImports.has(callee.object.name) &&
105
+ callee.property.type === 'Identifier' &&
106
+ callee.property.name === 'inject'
107
+ ) {
108
+ return callee.property;
109
+ }
110
+
111
+ return undefined;
112
+ }
113
+
114
+ function createInjectMessage(tokenText, helperName) {
115
+ if (!helperName) {
116
+ return 'Angular inject() is forbidden. Use the injectX helper exposed by a craftService/toCraftService adapter instead.';
117
+ }
118
+
119
+ return `Angular inject(${tokenText}) is forbidden. Use ${helperName} from a craftService/toCraftService adapter instead.`;
120
+ }
121
+
122
+ function getInjectTokenText(argument, sourceCode) {
123
+ if (!argument) {
124
+ return '';
125
+ }
126
+
127
+ return sourceCode.getText(argument);
128
+ }
129
+
130
+ function getRecommendedInjectorName(argument) {
131
+ const tokenName = getTokenName(argument);
132
+ if (!tokenName) {
133
+ return undefined;
134
+ }
135
+
136
+ return `inject${toPascalCase(tokenName)}`;
137
+ }
138
+
139
+ function getTokenName(argument) {
140
+ if (!argument) {
141
+ return undefined;
142
+ }
143
+
144
+ if (argument.type === 'Identifier') {
145
+ return argument.name;
146
+ }
147
+
148
+ if (
149
+ argument.type === 'MemberExpression' &&
150
+ !argument.computed &&
151
+ argument.property.type === 'Identifier'
152
+ ) {
153
+ return argument.property.name;
154
+ }
155
+
156
+ return undefined;
157
+ }
158
+
159
+ function toPascalCase(value) {
160
+ return value
161
+ .replace(/^[^A-Za-z$]+/, '')
162
+ .split(/[^A-Za-z0-9$]+|(?<=[a-z0-9])(?=[A-Z])/)
163
+ .filter(Boolean)
164
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
165
+ .join('');
166
+ }
167
+
168
+ function createTemporaryDisableSuggestion(
169
+ sourceCode,
170
+ node,
171
+ ruleName,
172
+ migrationNote,
173
+ ) {
174
+ return {
175
+ desc: 'Insert a temporary eslint-disable-next-line comment with a migration note.',
176
+ fix(fixer) {
177
+ const lineStart = sourceCode.getIndexFromLoc({
178
+ line: node.loc.start.line,
179
+ column: 0,
180
+ });
181
+ return fixer.insertTextBeforeRange(
182
+ [lineStart, lineStart],
183
+ `// eslint-disable-next-line craft-ng/${ruleName} -- ${migrationNote}\n`,
184
+ );
185
+ },
186
+ };
187
+ }