eslint-plugin-conventions 4.2.6 → 4.2.7
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 +2 -2
- package/src/index.js +1 -45
- package/src/lib/eslint-plugin-conventions.js +1 -6
- package/src/oxlint.js +1 -3
- package/src/rules/conventions/analytics-event-naming.js +1 -93
- package/src/rules/conventions/consistent-existence-index-check.js +1 -130
- package/src/rules/conventions/expiring-todo-comments.js +1 -283
- package/src/rules/conventions/filename-case.js +1 -234
- package/src/rules/conventions/no-commented-code.js +1 -237
- package/src/rules/conventions/no-console-spaces.js +1 -113
- package/src/rules/conventions/no-json-schema-tags.js +1 -159
- package/src/rules/conventions/no-magic-numbers.js +2 -185
- package/src/rules/conventions/no-raw-cross-property-href.js +1 -76
- package/src/rules/conventions/prefer-code-point.js +1 -80
- package/src/rules/conventions/prefer-dependency-version-strategy.js +1 -201
- package/src/rules/conventions/prefer-dom-node-text-content.js +1 -127
- package/src/rules/conventions/require-data-testid.js +1 -179
- package/src/rules/conventions/utm-taxonomy.js +1 -106
- package/src/rules/deprecation/no-deprecated-api.js +1 -161
|
@@ -1,201 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.preferDependencyVersionStrategy = void 0;
|
|
4
|
-
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
5
|
-
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
6
|
-
exports.preferDependencyVersionStrategy = (0, eslint_devkit_2.createRule)({
|
|
7
|
-
name: 'prefer-dependency-version-strategy',
|
|
8
|
-
meta: {
|
|
9
|
-
type: 'suggestion',
|
|
10
|
-
docs: {
|
|
11
|
-
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-conventions/docs/rules/prefer-dependency-version-strategy.md',
|
|
12
|
-
description: 'Enforce consistent version strategy (caret, tilde, exact, etc.) for package.json dependencies',
|
|
13
|
-
},
|
|
14
|
-
fixable: 'code',
|
|
15
|
-
messages: {
|
|
16
|
-
preferStrategy: (0, eslint_devkit_1.formatLLMMessage)({
|
|
17
|
-
icon: eslint_devkit_1.MessageIcons.PACKAGE,
|
|
18
|
-
issueName: 'Dependency Version Strategy',
|
|
19
|
-
description: 'Dependency "{{name}}" should use {{strategy}} version',
|
|
20
|
-
severity: 'MEDIUM',
|
|
21
|
-
fix: 'Change "{{current}}" to "{{expected}}" for version flexibility',
|
|
22
|
-
documentationLink: 'https://docs.npmjs.com/cli/v10/configuring-npm/package-json#dependencies',
|
|
23
|
-
}),
|
|
24
|
-
invalidStrategy: (0, eslint_devkit_1.formatLLMMessage)({
|
|
25
|
-
icon: eslint_devkit_1.MessageIcons.WARNING,
|
|
26
|
-
issueName: 'Invalid Version Strategy',
|
|
27
|
-
description: 'Strategy "{{strategy}}" is not valid',
|
|
28
|
-
severity: 'MEDIUM',
|
|
29
|
-
fix: 'Use one of: caret (^), tilde (~), exact (no prefix), range (<, >, ||), or any',
|
|
30
|
-
documentationLink: 'https://docs.npmjs.com/cli/v10/configuring-npm/package-json#dependencies',
|
|
31
|
-
}),
|
|
32
|
-
},
|
|
33
|
-
schema: [
|
|
34
|
-
{
|
|
35
|
-
type: 'object',
|
|
36
|
-
properties: {
|
|
37
|
-
strategy: {
|
|
38
|
-
type: 'string',
|
|
39
|
-
enum: ['caret', 'tilde', 'exact', 'range', 'any'],
|
|
40
|
-
description: 'Version strategy to enforce: caret (^), tilde (~), exact (no prefix), range (allows <, >, ||), or any (allows all)',
|
|
41
|
-
},
|
|
42
|
-
allowWorkspace: {
|
|
43
|
-
type: 'boolean',
|
|
44
|
-
description: 'Allow workspace: protocol versions',
|
|
45
|
-
default: true,
|
|
46
|
-
},
|
|
47
|
-
allowFile: {
|
|
48
|
-
type: 'boolean',
|
|
49
|
-
description: 'Allow file: protocol versions',
|
|
50
|
-
default: true,
|
|
51
|
-
},
|
|
52
|
-
allowLink: {
|
|
53
|
-
type: 'boolean',
|
|
54
|
-
description: 'Allow link: protocol versions',
|
|
55
|
-
default: true,
|
|
56
|
-
},
|
|
57
|
-
overrides: {
|
|
58
|
-
type: 'object',
|
|
59
|
-
additionalProperties: {
|
|
60
|
-
type: 'string',
|
|
61
|
-
enum: ['caret', 'tilde', 'exact', 'range', 'any'],
|
|
62
|
-
},
|
|
63
|
-
description: 'Package-specific strategy overrides. Key is package name, value is strategy. Example: { "react": "exact", "lodash": "tilde" }',
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
additionalProperties: false,
|
|
67
|
-
},
|
|
68
|
-
],
|
|
69
|
-
},
|
|
70
|
-
defaultOptions: [
|
|
71
|
-
{
|
|
72
|
-
strategy: 'caret',
|
|
73
|
-
allowWorkspace: true,
|
|
74
|
-
allowFile: true,
|
|
75
|
-
allowLink: true,
|
|
76
|
-
overrides: {},
|
|
77
|
-
},
|
|
78
|
-
],
|
|
79
|
-
create(context) {
|
|
80
|
-
const options = context.options[0] || {};
|
|
81
|
-
const { strategy = 'caret', allowWorkspace = true, allowFile = true, allowLink = true, overrides = {}, } = options;
|
|
82
|
-
const validStrategies = [
|
|
83
|
-
'caret',
|
|
84
|
-
'tilde',
|
|
85
|
-
'exact',
|
|
86
|
-
'range',
|
|
87
|
-
'any',
|
|
88
|
-
];
|
|
89
|
-
if (!validStrategies.includes(strategy)) {
|
|
90
|
-
context.report({
|
|
91
|
-
loc: { line: 1, column: 0 },
|
|
92
|
-
messageId: 'invalidStrategy',
|
|
93
|
-
data: { strategy },
|
|
94
|
-
});
|
|
95
|
-
return {};
|
|
96
|
-
}
|
|
97
|
-
if (strategy === 'any') {
|
|
98
|
-
return {};
|
|
99
|
-
}
|
|
100
|
-
function checkVersion(node, depName, version) {
|
|
101
|
-
if (allowWorkspace && version.startsWith('workspace:'))
|
|
102
|
-
return;
|
|
103
|
-
if (allowFile && version.startsWith('file:'))
|
|
104
|
-
return;
|
|
105
|
-
if (allowLink && version.startsWith('link:'))
|
|
106
|
-
return;
|
|
107
|
-
if (!version.match(/^[\^~<>=]?\d+\.\d+\.\d+/))
|
|
108
|
-
return;
|
|
109
|
-
const packageStrategy = overrides[depName] || strategy;
|
|
110
|
-
if (packageStrategy === 'any')
|
|
111
|
-
return;
|
|
112
|
-
let expectedVersion = version;
|
|
113
|
-
let needsFix = false;
|
|
114
|
-
const baseVersion = version.replace(/^[\^~<>=]+/, '');
|
|
115
|
-
switch (packageStrategy) {
|
|
116
|
-
case 'caret':
|
|
117
|
-
if (!version.startsWith('^')) {
|
|
118
|
-
expectedVersion = `^${baseVersion}`;
|
|
119
|
-
needsFix = true;
|
|
120
|
-
}
|
|
121
|
-
break;
|
|
122
|
-
case 'tilde':
|
|
123
|
-
if (!version.startsWith('~')) {
|
|
124
|
-
expectedVersion = `~${baseVersion}`;
|
|
125
|
-
needsFix = true;
|
|
126
|
-
}
|
|
127
|
-
break;
|
|
128
|
-
case 'exact': {
|
|
129
|
-
if (version !== baseVersion) {
|
|
130
|
-
expectedVersion = baseVersion;
|
|
131
|
-
needsFix = true;
|
|
132
|
-
}
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
case 'range':
|
|
136
|
-
if (!version.includes('||') &&
|
|
137
|
-
!version.includes('>=') &&
|
|
138
|
-
!version.includes('<=') &&
|
|
139
|
-
!version.includes('>') &&
|
|
140
|
-
!version.includes('<') &&
|
|
141
|
-
!version.includes(' - ')) {
|
|
142
|
-
expectedVersion = `^${baseVersion}`;
|
|
143
|
-
needsFix = true;
|
|
144
|
-
}
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
if (needsFix) {
|
|
148
|
-
context.report({
|
|
149
|
-
node: node.value,
|
|
150
|
-
messageId: 'preferStrategy',
|
|
151
|
-
data: {
|
|
152
|
-
name: depName,
|
|
153
|
-
strategy: packageStrategy,
|
|
154
|
-
current: version,
|
|
155
|
-
expected: expectedVersion,
|
|
156
|
-
},
|
|
157
|
-
fix(fixer) {
|
|
158
|
-
return fixer.replaceText(node.value, JSON.stringify(expectedVersion));
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
const checkObjectExpression = (node) => {
|
|
164
|
-
for (const prop of node.properties) {
|
|
165
|
-
if (prop.type === 'Property' &&
|
|
166
|
-
prop.key &&
|
|
167
|
-
prop.value &&
|
|
168
|
-
prop.value.type === 'Literal') {
|
|
169
|
-
const depName = prop.key.type === 'Identifier'
|
|
170
|
-
? prop.key.name
|
|
171
|
-
: prop.key.type === 'Literal'
|
|
172
|
-
? String(prop.key.value)
|
|
173
|
-
: null;
|
|
174
|
-
if (depName && typeof prop.value.value === 'string') {
|
|
175
|
-
checkVersion(prop, depName, prop.value.value);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
return {
|
|
181
|
-
'Property[key.value="dependencies"], Property[key.value="devDependencies"], Property[key.value="peerDependencies"]'(node) {
|
|
182
|
-
if (node.value.type !== 'ObjectExpression')
|
|
183
|
-
return;
|
|
184
|
-
checkObjectExpression(node.value);
|
|
185
|
-
},
|
|
186
|
-
ObjectExpression(node) {
|
|
187
|
-
const hasVersionLikeValues = node.properties.some((prop) => {
|
|
188
|
-
if (prop.type === 'Property' && prop.value.type === 'Literal') {
|
|
189
|
-
const value = String(prop.value.value);
|
|
190
|
-
return (/^[\^~]?\d+\.\d+\.\d+/.test(value) ||
|
|
191
|
-
value.startsWith('workspace:'));
|
|
192
|
-
}
|
|
193
|
-
return false;
|
|
194
|
-
});
|
|
195
|
-
if (hasVersionLikeValues) {
|
|
196
|
-
checkObjectExpression(node);
|
|
197
|
-
}
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
},
|
|
201
|
-
});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.preferDependencyVersionStrategy=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");exports.preferDependencyVersionStrategy=(0,eslint_devkit_2.createRule)({name:"prefer-dependency-version-strategy",meta:{type:"suggestion",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-conventions/docs/rules/prefer-dependency-version-strategy.md",description:"Enforce consistent version strategy (caret, tilde, exact, etc.) for package.json dependencies"},fixable:"code",messages:{preferStrategy:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.PACKAGE,issueName:"Dependency Version Strategy",description:'Dependency "{{name}}" should use {{strategy}} version',severity:"MEDIUM",fix:'Change "{{current}}" to "{{expected}}" for version flexibility',documentationLink:"https://docs.npmjs.com/cli/v10/configuring-npm/package-json#dependencies"}),invalidStrategy:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Invalid Version Strategy",description:'Strategy "{{strategy}}" is not valid',severity:"MEDIUM",fix:"Use one of: caret (^), tilde (~), exact (no prefix), range (<, >, ||), or any",documentationLink:"https://docs.npmjs.com/cli/v10/configuring-npm/package-json#dependencies"})},schema:[{type:"object",properties:{strategy:{type:"string",enum:["caret","tilde","exact","range","any"],description:"Version strategy to enforce: caret (^), tilde (~), exact (no prefix), range (allows <, >, ||), or any (allows all)"},allowWorkspace:{type:"boolean",description:"Allow workspace: protocol versions",default:true},allowFile:{type:"boolean",description:"Allow file: protocol versions",default:true},allowLink:{type:"boolean",description:"Allow link: protocol versions",default:true},overrides:{type:"object",additionalProperties:{type:"string",enum:["caret","tilde","exact","range","any"]},description:'Package-specific strategy overrides. Key is package name, value is strategy. Example: { "react": "exact", "lodash": "tilde" }'}},additionalProperties:false}]},defaultOptions:[{strategy:"caret",allowWorkspace:true,allowFile:true,allowLink:true,overrides:{}}],create(context){const options=context.options[0]||{};const{strategy="caret",allowWorkspace=true,allowFile=true,allowLink=true,overrides={}}=options;const validStrategies=["caret","tilde","exact","range","any"];if(!validStrategies.includes(strategy)){context.report({loc:{line:1,column:0},messageId:"invalidStrategy",data:{strategy}});return{}}if(strategy==="any"){return{}}function checkVersion(node,depName,version){if(allowWorkspace&&version.startsWith("workspace:"))return;if(allowFile&&version.startsWith("file:"))return;if(allowLink&&version.startsWith("link:"))return;if(!version.match(/^[\^~<>=]?\d+\.\d+\.\d+/))return;const packageStrategy=overrides[depName]||strategy;if(packageStrategy==="any")return;let expectedVersion=version;let needsFix=false;const baseVersion=version.replace(/^[\^~<>=]+/,"");switch(packageStrategy){case"caret":if(!version.startsWith("^")){expectedVersion=`^${baseVersion}`;needsFix=true}break;case"tilde":if(!version.startsWith("~")){expectedVersion=`~${baseVersion}`;needsFix=true}break;case"exact":{if(version!==baseVersion){expectedVersion=baseVersion;needsFix=true}break}case"range":if(!version.includes("||")&&!version.includes(">=")&&!version.includes("<=")&&!version.includes(">")&&!version.includes("<")&&!version.includes(" - ")){expectedVersion=`^${baseVersion}`;needsFix=true}break}if(needsFix){context.report({node:node.value,messageId:"preferStrategy",data:{name:depName,strategy:packageStrategy,current:version,expected:expectedVersion},fix(fixer){return fixer.replaceText(node.value,JSON.stringify(expectedVersion))}})}}const checkObjectExpression=node=>{for(const prop of node.properties){if(prop.type==="Property"&&prop.key&&prop.value&&prop.value.type==="Literal"){const depName=prop.key.type==="Identifier"?prop.key.name:prop.key.type==="Literal"?String(prop.key.value):null;if(depName&&typeof prop.value.value==="string"){checkVersion(prop,depName,prop.value.value)}}}};return{'Property[key.value="dependencies"], Property[key.value="devDependencies"], Property[key.value="peerDependencies"]'(node){if(node.value.type!=="ObjectExpression")return;checkObjectExpression(node.value)},ObjectExpression(node){const hasVersionLikeValues=node.properties.some(prop=>{if(prop.type==="Property"&&prop.value.type==="Literal"){const value=String(prop.value.value);return/^[\^~]?\d+\.\d+\.\d+/.test(value)||value.startsWith("workspace:")}return false});if(hasVersionLikeValues){checkObjectExpression(node)}}}}});
|
|
@@ -1,127 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.preferDomNodeTextContent = void 0;
|
|
4
|
-
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
5
|
-
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
6
|
-
exports.preferDomNodeTextContent = (0, eslint_devkit_1.createRule)({
|
|
7
|
-
name: 'prefer-dom-node-text-content',
|
|
8
|
-
meta: {
|
|
9
|
-
type: 'suggestion',
|
|
10
|
-
docs: {
|
|
11
|
-
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-conventions/docs/rules/prefer-dom-node-text-content.md',
|
|
12
|
-
description: 'Prefer textContent over innerText for better performance and reliability',
|
|
13
|
-
},
|
|
14
|
-
hasSuggestions: true,
|
|
15
|
-
messages: {
|
|
16
|
-
preferDomNodeTextContent: (0, eslint_devkit_2.formatLLMMessage)({
|
|
17
|
-
icon: eslint_devkit_2.MessageIcons.PERFORMANCE,
|
|
18
|
-
issueName: 'DOM Text Access',
|
|
19
|
-
description: 'Use textContent instead of innerText',
|
|
20
|
-
severity: 'MEDIUM',
|
|
21
|
-
fix: 'Replace innerText with textContent',
|
|
22
|
-
documentationLink: 'https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-dom-node-text-content.md',
|
|
23
|
-
}),
|
|
24
|
-
},
|
|
25
|
-
schema: [
|
|
26
|
-
{
|
|
27
|
-
type: 'object',
|
|
28
|
-
properties: {},
|
|
29
|
-
additionalProperties: false,
|
|
30
|
-
},
|
|
31
|
-
],
|
|
32
|
-
},
|
|
33
|
-
defaultOptions: [],
|
|
34
|
-
create(context) {
|
|
35
|
-
function isInAllowedContext() {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
function isLikelyDomElement(node) {
|
|
39
|
-
const obj = node.object;
|
|
40
|
-
if (obj.type === 'Identifier') {
|
|
41
|
-
const name = obj.name;
|
|
42
|
-
if (name.match(/^(element|el|div|span|node|ref|dom|elem)$/i)) {
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
if (name.match(/(Element|Node|Ref)$/)) {
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
if (obj.type === 'CallExpression' &&
|
|
50
|
-
obj.callee.type === 'MemberExpression') {
|
|
51
|
-
const methodName = obj.callee.property?.name;
|
|
52
|
-
if (methodName &&
|
|
53
|
-
[
|
|
54
|
-
'querySelector',
|
|
55
|
-
'querySelectorAll',
|
|
56
|
-
'getElementById',
|
|
57
|
-
'getElementsByClassName',
|
|
58
|
-
'getElementsByTagName',
|
|
59
|
-
'createElement',
|
|
60
|
-
].includes(methodName)) {
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (obj.type === 'MemberExpression') {
|
|
65
|
-
const propName = obj.property?.name;
|
|
66
|
-
if (propName &&
|
|
67
|
-
[
|
|
68
|
-
'current',
|
|
69
|
-
'children',
|
|
70
|
-
'childNodes',
|
|
71
|
-
'parentNode',
|
|
72
|
-
'element',
|
|
73
|
-
].includes(propName)) {
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
function isInnerTextAccess(node) {
|
|
80
|
-
if (node.property.type === 'Identifier' &&
|
|
81
|
-
node.property.name === 'innerText') {
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
if (node.computed &&
|
|
85
|
-
node.property.type === 'Literal' &&
|
|
86
|
-
node.property.value === 'innerText') {
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
MemberExpression(node) {
|
|
93
|
-
if (isInnerTextAccess(node) &&
|
|
94
|
-
isLikelyDomElement(node) &&
|
|
95
|
-
!isInAllowedContext()) {
|
|
96
|
-
context.report({
|
|
97
|
-
node,
|
|
98
|
-
messageId: 'preferDomNodeTextContent',
|
|
99
|
-
data: {
|
|
100
|
-
current: 'innerText',
|
|
101
|
-
fix: 'textContent',
|
|
102
|
-
},
|
|
103
|
-
suggest: [
|
|
104
|
-
{
|
|
105
|
-
messageId: 'preferDomNodeTextContent',
|
|
106
|
-
data: {
|
|
107
|
-
replacement: 'textContent',
|
|
108
|
-
suggestion: 'Replace with textContent',
|
|
109
|
-
},
|
|
110
|
-
fix(fixer) {
|
|
111
|
-
if (node.property.type === 'Identifier') {
|
|
112
|
-
return fixer.replaceText(node.property, 'textContent');
|
|
113
|
-
}
|
|
114
|
-
else if (node.property.type === 'Literal' &&
|
|
115
|
-
node.property.value === 'innerText') {
|
|
116
|
-
return fixer.replaceText(node.property, '"textContent"');
|
|
117
|
-
}
|
|
118
|
-
return null;
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
],
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
},
|
|
127
|
-
});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.preferDomNodeTextContent=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");exports.preferDomNodeTextContent=(0,eslint_devkit_1.createRule)({name:"prefer-dom-node-text-content",meta:{type:"suggestion",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-conventions/docs/rules/prefer-dom-node-text-content.md",description:"Prefer textContent over innerText for better performance and reliability"},hasSuggestions:true,messages:{preferDomNodeTextContent:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.PERFORMANCE,issueName:"DOM Text Access",description:"Use textContent instead of innerText",severity:"MEDIUM",fix:"Replace innerText with textContent",documentationLink:"https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-dom-node-text-content.md"})},schema:[{type:"object",properties:{},additionalProperties:false}]},defaultOptions:[],create(context){function isInAllowedContext(){return false}function isLikelyDomElement(node){const obj=node.object;if(obj.type==="Identifier"){const name=obj.name;if(name.match(/^(element|el|div|span|node|ref|dom|elem)$/i)){return true}if(name.match(/(Element|Node|Ref)$/)){return true}}if(obj.type==="CallExpression"&&obj.callee.type==="MemberExpression"){const methodName=obj.callee.property?.name;if(methodName&&["querySelector","querySelectorAll","getElementById","getElementsByClassName","getElementsByTagName","createElement"].includes(methodName)){return true}}if(obj.type==="MemberExpression"){const propName=obj.property?.name;if(propName&&["current","children","childNodes","parentNode","element"].includes(propName)){return true}}return false}function isInnerTextAccess(node){if(node.property.type==="Identifier"&&node.property.name==="innerText"){return true}if(node.computed&&node.property.type==="Literal"&&node.property.value==="innerText"){return true}return false}return{MemberExpression(node){if(isInnerTextAccess(node)&&isLikelyDomElement(node)&&!isInAllowedContext()){context.report({node,messageId:"preferDomNodeTextContent",data:{current:"innerText",fix:"textContent"},suggest:[{messageId:"preferDomNodeTextContent",data:{replacement:"textContent",suggestion:"Replace with textContent"},fix(fixer){if(node.property.type==="Identifier"){return fixer.replaceText(node.property,"textContent")}else if(node.property.type==="Literal"&&node.property.value==="innerText"){return fixer.replaceText(node.property,'"textContent"')}return null}}]})}}}}});
|
|
@@ -1,179 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.requireDataTestId = void 0;
|
|
4
|
-
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
5
|
-
const ALWAYS_INTERACTIVE = new Set([
|
|
6
|
-
'button',
|
|
7
|
-
'input',
|
|
8
|
-
'select',
|
|
9
|
-
'textarea',
|
|
10
|
-
'form',
|
|
11
|
-
]);
|
|
12
|
-
function isInteractiveAnchor(node) {
|
|
13
|
-
return node.attributes.some((attr) => {
|
|
14
|
-
if (attr.type !== 'JSXAttribute')
|
|
15
|
-
return false;
|
|
16
|
-
const name = attr.name.type === 'JSXIdentifier' ? attr.name.name : undefined;
|
|
17
|
-
return name === 'href' || name === 'onClick';
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
function hasEventHandler(node) {
|
|
21
|
-
return node.attributes.some((attr) => {
|
|
22
|
-
if (attr.type !== 'JSXAttribute')
|
|
23
|
-
return false;
|
|
24
|
-
if (attr.name.type !== 'JSXIdentifier')
|
|
25
|
-
return false;
|
|
26
|
-
return /^on[A-Z]/.test(attr.name.name);
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
function getJsxName(name) {
|
|
30
|
-
if (name.type === 'JSXIdentifier')
|
|
31
|
-
return name.name;
|
|
32
|
-
if (name.type === 'JSXMemberExpression') {
|
|
33
|
-
return name.property.type === 'JSXIdentifier'
|
|
34
|
-
? name.property.name
|
|
35
|
-
: undefined;
|
|
36
|
-
}
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
39
|
-
function hasDataTestId(node) {
|
|
40
|
-
for (const attr of node.attributes) {
|
|
41
|
-
if (attr.type !== 'JSXAttribute')
|
|
42
|
-
continue;
|
|
43
|
-
if (attr.name.type === 'JSXIdentifier' && attr.name.name === 'data-testid')
|
|
44
|
-
return attr;
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
function hasSpreadingAllProps(node) {
|
|
49
|
-
return node.attributes.some((a) => a.type === 'JSXSpreadAttribute');
|
|
50
|
-
}
|
|
51
|
-
function isStableValue(attr) {
|
|
52
|
-
if (!attr.value)
|
|
53
|
-
return false;
|
|
54
|
-
if (attr.value.type === 'Literal')
|
|
55
|
-
return typeof attr.value.value === 'string';
|
|
56
|
-
if (attr.value.type === 'JSXExpressionContainer') {
|
|
57
|
-
const expr = attr.value.expression;
|
|
58
|
-
if (expr.type === 'Literal')
|
|
59
|
-
return typeof expr.value === 'string';
|
|
60
|
-
if (expr.type === 'TemplateLiteral') {
|
|
61
|
-
return expr.expressions.every((e) => ['Identifier', 'MemberExpression', 'Literal'].includes(e.type));
|
|
62
|
-
}
|
|
63
|
-
if (expr.type === 'Identifier')
|
|
64
|
-
return true;
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
exports.requireDataTestId = (0, eslint_devkit_1.createRule)({
|
|
70
|
-
name: 'require-data-testid',
|
|
71
|
-
meta: {
|
|
72
|
-
type: 'problem',
|
|
73
|
-
docs: {
|
|
74
|
-
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-conventions/docs/rules/require-data-testid.md',
|
|
75
|
-
description: 'Require stable `data-testid` attributes on interactive elements and custom components for testability.',
|
|
76
|
-
},
|
|
77
|
-
fixable: undefined,
|
|
78
|
-
hasSuggestions: false,
|
|
79
|
-
messages: {
|
|
80
|
-
missingDataTestId: (0, eslint_devkit_1.formatLLMMessage)({
|
|
81
|
-
icon: eslint_devkit_1.MessageIcons.WARNING,
|
|
82
|
-
issueName: 'Missing data-testid',
|
|
83
|
-
description: 'Interactive elements and custom components must expose a stable `data-testid` for E2E testability. Class-name selectors break on every Tailwind refactor; `data-testid` survives.',
|
|
84
|
-
severity: 'MEDIUM',
|
|
85
|
-
fix: 'Add `data-testid="<lower-kebab-case-name>"` to the element.',
|
|
86
|
-
documentationLink: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-conventions/docs/rules/require-data-testid.md',
|
|
87
|
-
}),
|
|
88
|
-
dynamicDataTestId: (0, eslint_devkit_1.formatLLMMessage)({
|
|
89
|
-
icon: eslint_devkit_1.MessageIcons.WARNING,
|
|
90
|
-
issueName: 'Unstable data-testid value',
|
|
91
|
-
description: '`data-testid` should be a string literal or a template literal of stable identifiers — values that change between renders make tests flaky.',
|
|
92
|
-
severity: 'LOW',
|
|
93
|
-
fix: 'Use a string literal or template like `pagination-page-${pageNumber}` instead of dynamic / computed values.',
|
|
94
|
-
documentationLink: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-conventions/docs/rules/require-data-testid.md',
|
|
95
|
-
}),
|
|
96
|
-
},
|
|
97
|
-
schema: [
|
|
98
|
-
{
|
|
99
|
-
type: 'object',
|
|
100
|
-
additionalProperties: false,
|
|
101
|
-
properties: {
|
|
102
|
-
requireOn: {
|
|
103
|
-
type: 'array',
|
|
104
|
-
items: { type: 'string' },
|
|
105
|
-
description: 'Extra element / component names to flag.',
|
|
106
|
-
},
|
|
107
|
-
ignore: {
|
|
108
|
-
type: 'array',
|
|
109
|
-
items: { type: 'string' },
|
|
110
|
-
description: 'Element / component names to skip.',
|
|
111
|
-
},
|
|
112
|
-
componentPattern: {
|
|
113
|
-
type: 'string',
|
|
114
|
-
description: 'Regex; matched names are treated as custom components. Empty disables component checks.',
|
|
115
|
-
},
|
|
116
|
-
componentRequiresHandler: {
|
|
117
|
-
type: 'boolean',
|
|
118
|
-
description: 'When `componentPattern` matches, only flag if the component has an `on*` event handler (default true). Set false to flag every matching component.',
|
|
119
|
-
},
|
|
120
|
-
enforceStableValues: {
|
|
121
|
-
type: 'boolean',
|
|
122
|
-
description: 'Reject computed `data-testid` values (default: true).',
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
],
|
|
127
|
-
},
|
|
128
|
-
defaultOptions: [
|
|
129
|
-
{
|
|
130
|
-
requireOn: [],
|
|
131
|
-
ignore: [],
|
|
132
|
-
componentPattern: '^[A-Z]',
|
|
133
|
-
componentRequiresHandler: true,
|
|
134
|
-
enforceStableValues: true,
|
|
135
|
-
},
|
|
136
|
-
],
|
|
137
|
-
create(context, [opts]) {
|
|
138
|
-
const options = opts ?? {};
|
|
139
|
-
const requireOn = new Set(options.requireOn ?? []);
|
|
140
|
-
const ignore = new Set(options.ignore ?? []);
|
|
141
|
-
const componentPatternStr = options.componentPattern ?? '^[A-Z]';
|
|
142
|
-
const componentPattern = componentPatternStr
|
|
143
|
-
? new RegExp(componentPatternStr)
|
|
144
|
-
: null;
|
|
145
|
-
const componentRequiresHandler = options.componentRequiresHandler ?? true;
|
|
146
|
-
const enforceStableValues = options.enforceStableValues ?? true;
|
|
147
|
-
return {
|
|
148
|
-
JSXOpeningElement(node) {
|
|
149
|
-
const name = getJsxName(node.name);
|
|
150
|
-
if (!name)
|
|
151
|
-
return;
|
|
152
|
-
if (ignore.has(name))
|
|
153
|
-
return;
|
|
154
|
-
let inScope = false;
|
|
155
|
-
if (ALWAYS_INTERACTIVE.has(name))
|
|
156
|
-
inScope = true;
|
|
157
|
-
else if (name === 'a' && isInteractiveAnchor(node))
|
|
158
|
-
inScope = true;
|
|
159
|
-
else if (componentPattern && componentPattern.test(name)) {
|
|
160
|
-
inScope = componentRequiresHandler ? hasEventHandler(node) : true;
|
|
161
|
-
}
|
|
162
|
-
else if (requireOn.has(name))
|
|
163
|
-
inScope = true;
|
|
164
|
-
if (!inScope)
|
|
165
|
-
return;
|
|
166
|
-
if (hasSpreadingAllProps(node))
|
|
167
|
-
return;
|
|
168
|
-
const existing = hasDataTestId(node);
|
|
169
|
-
if (!existing) {
|
|
170
|
-
context.report({ node, messageId: 'missingDataTestId' });
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
if (enforceStableValues && !isStableValue(existing)) {
|
|
174
|
-
context.report({ node: existing, messageId: 'dynamicDataTestId' });
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
};
|
|
178
|
-
},
|
|
179
|
-
});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.requireDataTestId=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const ALWAYS_INTERACTIVE=new Set(["button","input","select","textarea","form"]);function isInteractiveAnchor(node){return node.attributes.some(attr=>{if(attr.type!=="JSXAttribute")return false;const name=attr.name.type==="JSXIdentifier"?attr.name.name:void 0;return name==="href"||name==="onClick"})}function hasEventHandler(node){return node.attributes.some(attr=>{if(attr.type!=="JSXAttribute")return false;if(attr.name.type!=="JSXIdentifier")return false;return/^on[A-Z]/.test(attr.name.name)})}function getJsxName(name){if(name.type==="JSXIdentifier")return name.name;if(name.type==="JSXMemberExpression"){return name.property.type==="JSXIdentifier"?name.property.name:void 0}return void 0}function hasDataTestId(node){for(const attr of node.attributes){if(attr.type!=="JSXAttribute")continue;if(attr.name.type==="JSXIdentifier"&&attr.name.name==="data-testid")return attr}return null}function hasSpreadingAllProps(node){return node.attributes.some(a=>a.type==="JSXSpreadAttribute")}function isStableValue(attr){if(!attr.value)return false;if(attr.value.type==="Literal")return typeof attr.value.value==="string";if(attr.value.type==="JSXExpressionContainer"){const expr=attr.value.expression;if(expr.type==="Literal")return typeof expr.value==="string";if(expr.type==="TemplateLiteral"){return expr.expressions.every(e=>["Identifier","MemberExpression","Literal"].includes(e.type))}if(expr.type==="Identifier")return true;return false}return false}exports.requireDataTestId=(0,eslint_devkit_1.createRule)({name:"require-data-testid",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-conventions/docs/rules/require-data-testid.md",description:"Require stable `data-testid` attributes on interactive elements and custom components for testability."},fixable:void 0,hasSuggestions:false,messages:{missingDataTestId:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Missing data-testid",description:"Interactive elements and custom components must expose a stable `data-testid` for E2E testability. Class-name selectors break on every Tailwind refactor; `data-testid` survives.",severity:"MEDIUM",fix:'Add `data-testid="<lower-kebab-case-name>"` to the element.',documentationLink:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-conventions/docs/rules/require-data-testid.md"}),dynamicDataTestId:(0,eslint_devkit_1.formatLLMMessage)({icon:eslint_devkit_1.MessageIcons.WARNING,issueName:"Unstable data-testid value",description:"`data-testid` should be a string literal or a template literal of stable identifiers \u2014 values that change between renders make tests flaky.",severity:"LOW",fix:"Use a string literal or template like `pagination-page-${pageNumber}` instead of dynamic / computed values.",documentationLink:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-conventions/docs/rules/require-data-testid.md"})},schema:[{type:"object",additionalProperties:false,properties:{requireOn:{type:"array",items:{type:"string"},description:"Extra element / component names to flag."},ignore:{type:"array",items:{type:"string"},description:"Element / component names to skip."},componentPattern:{type:"string",description:"Regex; matched names are treated as custom components. Empty disables component checks."},componentRequiresHandler:{type:"boolean",description:"When `componentPattern` matches, only flag if the component has an `on*` event handler (default true). Set false to flag every matching component."},enforceStableValues:{type:"boolean",description:"Reject computed `data-testid` values (default: true)."}}}]},defaultOptions:[{requireOn:[],ignore:[],componentPattern:"^[A-Z]",componentRequiresHandler:true,enforceStableValues:true}],create(context,[opts]){const options=opts??{};const requireOn=new Set(options.requireOn??[]);const ignore=new Set(options.ignore??[]);const componentPatternStr=options.componentPattern??"^[A-Z]";const componentPattern=componentPatternStr?new RegExp(componentPatternStr):null;const componentRequiresHandler=options.componentRequiresHandler??true;const enforceStableValues=options.enforceStableValues??true;return{JSXOpeningElement(node){const name=getJsxName(node.name);if(!name)return;if(ignore.has(name))return;let inScope=false;if(ALWAYS_INTERACTIVE.has(name))inScope=true;else if(name==="a"&&isInteractiveAnchor(node))inScope=true;else if(componentPattern&&componentPattern.test(name)){inScope=componentRequiresHandler?hasEventHandler(node):true}else if(requireOn.has(name))inScope=true;if(!inScope)return;if(hasSpreadingAllProps(node))return;const existing=hasDataTestId(node);if(!existing){context.report({node,messageId:"missingDataTestId"});return}if(enforceStableValues&&!isStableValue(existing)){context.report({node:existing,messageId:"dynamicDataTestId"})}}}}});
|
|
@@ -1,106 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.utmTaxonomy = void 0;
|
|
4
|
-
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
5
|
-
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
6
|
-
const VALID_UTM_SOURCES = new Set([
|
|
7
|
-
'ofriperetz_dev',
|
|
8
|
-
'interlace',
|
|
9
|
-
'eslint_docs',
|
|
10
|
-
'serverless_docs',
|
|
11
|
-
'ds',
|
|
12
|
-
'storybook',
|
|
13
|
-
'devto',
|
|
14
|
-
'github',
|
|
15
|
-
'npm',
|
|
16
|
-
'x',
|
|
17
|
-
'linkedin',
|
|
18
|
-
'email',
|
|
19
|
-
]);
|
|
20
|
-
const VALID_UTM_MEDIUMS = new Set([
|
|
21
|
-
'article',
|
|
22
|
-
'blog',
|
|
23
|
-
'docs',
|
|
24
|
-
'landing',
|
|
25
|
-
'social',
|
|
26
|
-
'email',
|
|
27
|
-
'referral',
|
|
28
|
-
'cli',
|
|
29
|
-
]);
|
|
30
|
-
const UTM_PARAM_RE = /[?&](utm_source|utm_medium)=([^&#]+)/g;
|
|
31
|
-
exports.utmTaxonomy = (0, eslint_devkit_1.createRule)({
|
|
32
|
-
name: 'utm-taxonomy',
|
|
33
|
-
meta: {
|
|
34
|
-
type: 'problem',
|
|
35
|
-
docs: {
|
|
36
|
-
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-conventions/docs/rules/utm-taxonomy.md',
|
|
37
|
-
description: 'Validate utm_source and utm_medium values against the fixed taxonomy in UTM_PHILOSOPHY.md',
|
|
38
|
-
},
|
|
39
|
-
fixable: undefined,
|
|
40
|
-
hasSuggestions: false,
|
|
41
|
-
messages: {
|
|
42
|
-
invalidUtmSource: (0, eslint_devkit_2.formatLLMMessage)({
|
|
43
|
-
icon: eslint_devkit_2.MessageIcons.WARNING,
|
|
44
|
-
issueName: 'Invalid utm_source',
|
|
45
|
-
description: 'utm_source="{{value}}" is not in the fixed taxonomy. Allowed: {{allowed}}',
|
|
46
|
-
severity: 'HIGH',
|
|
47
|
-
fix: 'Use one of the allowed values, or extend the taxonomy in UTM_PHILOSOPHY.md and this rule together.',
|
|
48
|
-
documentationLink: 'https://github.com/ofri-peretz/eslint/blob/main/UTM_PHILOSOPHY.md',
|
|
49
|
-
}),
|
|
50
|
-
invalidUtmMedium: (0, eslint_devkit_2.formatLLMMessage)({
|
|
51
|
-
icon: eslint_devkit_2.MessageIcons.WARNING,
|
|
52
|
-
issueName: 'Invalid utm_medium',
|
|
53
|
-
description: 'utm_medium="{{value}}" is not in the fixed taxonomy. Allowed: {{allowed}}',
|
|
54
|
-
severity: 'HIGH',
|
|
55
|
-
fix: 'Use one of the allowed values, or extend the taxonomy in UTM_PHILOSOPHY.md and this rule together.',
|
|
56
|
-
documentationLink: 'https://github.com/ofri-peretz/eslint/blob/main/UTM_PHILOSOPHY.md',
|
|
57
|
-
}),
|
|
58
|
-
},
|
|
59
|
-
schema: [],
|
|
60
|
-
},
|
|
61
|
-
defaultOptions: [],
|
|
62
|
-
create(context) {
|
|
63
|
-
function checkString(node, value) {
|
|
64
|
-
UTM_PARAM_RE.lastIndex = 0;
|
|
65
|
-
let match;
|
|
66
|
-
while ((match = UTM_PARAM_RE.exec(value)) !== null) {
|
|
67
|
-
const [, key, raw] = match;
|
|
68
|
-
const decoded = decodeURIComponent(raw);
|
|
69
|
-
if (key === 'utm_source' && !VALID_UTM_SOURCES.has(decoded)) {
|
|
70
|
-
context.report({
|
|
71
|
-
node,
|
|
72
|
-
messageId: 'invalidUtmSource',
|
|
73
|
-
data: {
|
|
74
|
-
value: decoded,
|
|
75
|
-
allowed: [...VALID_UTM_SOURCES].join(', '),
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
else if (key === 'utm_medium' && !VALID_UTM_MEDIUMS.has(decoded)) {
|
|
80
|
-
context.report({
|
|
81
|
-
node,
|
|
82
|
-
messageId: 'invalidUtmMedium',
|
|
83
|
-
data: {
|
|
84
|
-
value: decoded,
|
|
85
|
-
allowed: [...VALID_UTM_MEDIUMS].join(', '),
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
Literal(node) {
|
|
93
|
-
if (typeof node.value === 'string' && node.value.includes('utm_')) {
|
|
94
|
-
checkString(node, node.value);
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
TemplateLiteral(node) {
|
|
98
|
-
for (const quasi of node.quasis) {
|
|
99
|
-
if (quasi.value.cooked && quasi.value.cooked.includes('utm_')) {
|
|
100
|
-
checkString(quasi, quasi.value.cooked);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
},
|
|
106
|
-
});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.utmTaxonomy=void 0;const eslint_devkit_1=require("@interlace/eslint-devkit");const eslint_devkit_2=require("@interlace/eslint-devkit");const VALID_UTM_SOURCES=new Set(["ofriperetz_dev","interlace","eslint_docs","serverless_docs","ds","storybook","devto","github","npm","x","linkedin","email"]);const VALID_UTM_MEDIUMS=new Set(["article","blog","docs","landing","social","email","referral","cli"]);const UTM_PARAM_RE=/[?&](utm_source|utm_medium)=([^&#]+)/g;exports.utmTaxonomy=(0,eslint_devkit_1.createRule)({name:"utm-taxonomy",meta:{type:"problem",docs:{url:"https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-conventions/docs/rules/utm-taxonomy.md",description:"Validate utm_source and utm_medium values against the fixed taxonomy in UTM_PHILOSOPHY.md"},fixable:void 0,hasSuggestions:false,messages:{invalidUtmSource:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.WARNING,issueName:"Invalid utm_source",description:'utm_source="{{value}}" is not in the fixed taxonomy. Allowed: {{allowed}}',severity:"HIGH",fix:"Use one of the allowed values, or extend the taxonomy in UTM_PHILOSOPHY.md and this rule together.",documentationLink:"https://github.com/ofri-peretz/eslint/blob/main/UTM_PHILOSOPHY.md"}),invalidUtmMedium:(0,eslint_devkit_2.formatLLMMessage)({icon:eslint_devkit_2.MessageIcons.WARNING,issueName:"Invalid utm_medium",description:'utm_medium="{{value}}" is not in the fixed taxonomy. Allowed: {{allowed}}',severity:"HIGH",fix:"Use one of the allowed values, or extend the taxonomy in UTM_PHILOSOPHY.md and this rule together.",documentationLink:"https://github.com/ofri-peretz/eslint/blob/main/UTM_PHILOSOPHY.md"})},schema:[]},defaultOptions:[],create(context){function checkString(node,value){UTM_PARAM_RE.lastIndex=0;let match;while((match=UTM_PARAM_RE.exec(value))!==null){const[,key,raw]=match;const decoded=decodeURIComponent(raw);if(key==="utm_source"&&!VALID_UTM_SOURCES.has(decoded)){context.report({node,messageId:"invalidUtmSource",data:{value:decoded,allowed:[...VALID_UTM_SOURCES].join(", ")}})}else if(key==="utm_medium"&&!VALID_UTM_MEDIUMS.has(decoded)){context.report({node,messageId:"invalidUtmMedium",data:{value:decoded,allowed:[...VALID_UTM_MEDIUMS].join(", ")}})}}}return{Literal(node){if(typeof node.value==="string"&&node.value.includes("utm_")){checkString(node,node.value)}},TemplateLiteral(node){for(const quasi of node.quasis){if(quasi.value.cooked&&quasi.value.cooked.includes("utm_")){checkString(quasi,quasi.value.cooked)}}}}}});
|