eslint-plugin-stamhoofd 2.136.1 → 2.136.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-stamhoofd",
3
- "version": "2.136.1",
3
+ "version": "2.136.3",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "sideEffects": false,
@@ -30,5 +30,5 @@
30
30
  "publishConfig": {
31
31
  "access": "public"
32
32
  },
33
- "gitHead": "8c3886185fd30008018414c73f88aed6c0cb5185"
33
+ "gitHead": "ef990c776a392a3857dc81309ca6a0bb4d524f63"
34
34
  }
package/src/index.js CHANGED
@@ -14,12 +14,14 @@ import typescript from './configs/typescript.js';
14
14
  import asyncComponentWithProperties from './rules/async-component-with-properties.js';
15
15
  import asyncRouteComponents from './rules/async-route-components.js';
16
16
  import noPackageSelfImport from './rules/no-package-self-import.js';
17
+ import noTRouteUrl from './rules/no-t-route-url.js';
17
18
  import preferDefineRoute from './rules/prefer-define-route.js';
18
19
 
19
20
  const rules = {
20
21
  'async-component-with-properties': asyncComponentWithProperties,
21
22
  'async-route-components': asyncRouteComponents,
22
23
  'no-package-self-import': noPackageSelfImport,
24
+ 'no-t-route-url': noTRouteUrl,
23
25
  'prefer-define-route': preferDefineRoute,
24
26
  };
25
27
  const stamhoofdPlugin = {
@@ -178,6 +180,7 @@ export default {
178
180
  ],
179
181
  }],
180
182
  'stamhoofd/async-route-components': 'error',
183
+ 'stamhoofd/no-t-route-url': 'error',
181
184
  'stamhoofd/prefer-define-route': 'warn',
182
185
  // TODO: restore to 'error' once the existing dependency cycles are resolved.
183
186
  'import/no-cycle': ['warn', { maxDepth: 100, ignoreExternal: false, allowUnsafeDynamicCyclicDependency: true }],
@@ -0,0 +1,165 @@
1
+ const navigationPackage = '@simonbackx/vue-app-navigation';
2
+ const routeDefiners = new Set(['defineRoute', 'defineRoutes']);
3
+
4
+ function unwrapExpression(node) {
5
+ while (
6
+ node?.type === 'TSAsExpression'
7
+ || node?.type === 'TSNonNullExpression'
8
+ || node?.type === 'TSSatisfiesExpression'
9
+ || node?.type === 'TSTypeAssertion'
10
+ || node?.type === 'ChainExpression'
11
+ ) {
12
+ node = node.expression;
13
+ }
14
+
15
+ return node;
16
+ }
17
+
18
+ function findVariable(sourceCode, identifier) {
19
+ let scope = sourceCode.getScope(identifier);
20
+
21
+ while (scope) {
22
+ const variable = scope.set.get(identifier.name);
23
+ if (variable) {
24
+ return variable;
25
+ }
26
+ scope = scope.upper;
27
+ }
28
+
29
+ return null;
30
+ }
31
+
32
+ function getImportedName(variable) {
33
+ const definition = variable?.defs.find(def => def.type === 'ImportBinding');
34
+ const importDeclaration = definition?.parent;
35
+
36
+ if (importDeclaration?.type !== 'ImportDeclaration' || importDeclaration.source.value !== navigationPackage) {
37
+ return null;
38
+ }
39
+
40
+ if (definition.node.type !== 'ImportSpecifier') {
41
+ return null;
42
+ }
43
+
44
+ return definition.node.imported.name ?? definition.node.imported.value;
45
+ }
46
+
47
+ // Returns the name of the route definer (defineRoute/defineRoutes) that this
48
+ // callee resolves to, but only when it is imported from the navigation package.
49
+ function getRouteDefinerName(sourceCode, node) {
50
+ if (node.type !== 'Identifier') {
51
+ return null;
52
+ }
53
+
54
+ const imported = getImportedName(findVariable(sourceCode, node));
55
+ return routeDefiners.has(imported) ? imported : null;
56
+ }
57
+
58
+ function isTranslateCall(node) {
59
+ if (node.type !== 'CallExpression') {
60
+ return false;
61
+ }
62
+
63
+ const callee = unwrapExpression(node.callee);
64
+ return callee.type === 'Identifier' && callee.name === '$t';
65
+ }
66
+
67
+ // Detects a url value whose leading expression is a $t(...) call, i.e. the
68
+ // source text effectively starts with `$t(`. Descends into the left side of
69
+ // binary/logical expressions so `$t('a') + '/b'` is caught as well.
70
+ function startsWithTranslateCall(node) {
71
+ node = unwrapExpression(node);
72
+
73
+ if (isTranslateCall(node)) {
74
+ return true;
75
+ }
76
+
77
+ if (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') {
78
+ return startsWithTranslateCall(node.left);
79
+ }
80
+
81
+ return false;
82
+ }
83
+
84
+ function getPropertyKeyName(property) {
85
+ if (property.type !== 'Property' || property.computed) {
86
+ return null;
87
+ }
88
+
89
+ const key = property.key;
90
+ if (key.type === 'Identifier') {
91
+ return key.name;
92
+ }
93
+ if (key.type === 'Literal') {
94
+ return typeof key.value === 'string' ? key.value : null;
95
+ }
96
+
97
+ return null;
98
+ }
99
+
100
+ export default {
101
+ meta: {
102
+ type: 'problem',
103
+ docs: {
104
+ description: 'Disallow using the $t API for route urls; use buildTranslatedUrl instead.',
105
+ },
106
+ schema: [],
107
+ messages: {
108
+ noTranslateInUrl: 'Do not use the $t API for route urls: the resulting url is not reliable and depends on the active language. Use buildTranslatedUrl({ nl: \'...\', fr: \'...\', en: \'...\' }) instead, e.g. url: buildTranslatedUrl({ nl: \'activiteiten\', fr: \'activites\', en: \'activities\' }).',
109
+ },
110
+ },
111
+ create(context) {
112
+ const sourceCode = context.sourceCode;
113
+
114
+ function checkRouteObject(node) {
115
+ const object = unwrapExpression(node);
116
+ if (object.type !== 'ObjectExpression') {
117
+ return;
118
+ }
119
+
120
+ for (const property of object.properties) {
121
+ if (getPropertyKeyName(property) !== 'url') {
122
+ continue;
123
+ }
124
+
125
+ if (startsWithTranslateCall(property.value)) {
126
+ context.report({
127
+ node: property.value,
128
+ messageId: 'noTranslateInUrl',
129
+ });
130
+ }
131
+ }
132
+ }
133
+
134
+ return {
135
+ CallExpression(node) {
136
+ const definerName = getRouteDefinerName(sourceCode, unwrapExpression(node.callee));
137
+ if (!definerName) {
138
+ return;
139
+ }
140
+
141
+ const argument = node.arguments[0];
142
+ if (!argument) {
143
+ return;
144
+ }
145
+
146
+ if (definerName === 'defineRoute') {
147
+ checkRouteObject(argument);
148
+ return;
149
+ }
150
+
151
+ // defineRoutes takes an array of route objects.
152
+ const array = unwrapExpression(argument);
153
+ if (array.type !== 'ArrayExpression') {
154
+ return;
155
+ }
156
+
157
+ for (const element of array.elements) {
158
+ if (element && element.type !== 'SpreadElement') {
159
+ checkRouteObject(element);
160
+ }
161
+ }
162
+ },
163
+ };
164
+ },
165
+ };