@blumintinc/eslint-plugin-blumint 1.19.11 → 1.19.13
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/lib/index.js
CHANGED
|
@@ -23,6 +23,52 @@ function isAbsoluteOrFixedPosition(propertyName, propertyValue) {
|
|
|
23
23
|
function isPointerEventsProperty(propertyName) {
|
|
24
24
|
return propertyName === 'pointerEvents' || propertyName === 'pointer-events';
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The inset offsets that position a pseudo-element relative to its origin box.
|
|
28
|
+
*/
|
|
29
|
+
const INSET_PROPERTIES = new Set(['top', 'right', 'bottom', 'left']);
|
|
30
|
+
/**
|
|
31
|
+
* Classifies the sign of a leading numeric length in a string offset value
|
|
32
|
+
* (e.g. '-6px' -> negative, '0'/'0px' -> zero, '6px' -> positive). Anything
|
|
33
|
+
* that does not start with an optional-minus number is unknown.
|
|
34
|
+
*/
|
|
35
|
+
function classifyOffsetString(raw) {
|
|
36
|
+
const match = raw.trim().match(/^(-?)(\d+(?:\.\d+)?|\.\d+)/);
|
|
37
|
+
if (!match)
|
|
38
|
+
return 'unknown';
|
|
39
|
+
const numericPart = parseFloat(match[2]);
|
|
40
|
+
if (numericPart === 0)
|
|
41
|
+
return 'zero';
|
|
42
|
+
return match[1] === '-' ? 'negative' : 'positive';
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Classifies an inset offset property value (top/right/bottom/left) as a
|
|
46
|
+
* positive, negative, zero, or unknown length. Only the shapes that can be
|
|
47
|
+
* resolved statically are classified; variables, member expressions, template
|
|
48
|
+
* literals, and calls are treated as unknown and never counted toward the
|
|
49
|
+
* hit-slop exemption.
|
|
50
|
+
*/
|
|
51
|
+
function classifyOffsetValue(value) {
|
|
52
|
+
if (value.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
53
|
+
if (typeof value.value === 'number') {
|
|
54
|
+
if (value.value === 0)
|
|
55
|
+
return 'zero';
|
|
56
|
+
return value.value < 0 ? 'negative' : 'positive';
|
|
57
|
+
}
|
|
58
|
+
if (typeof value.value === 'string') {
|
|
59
|
+
return classifyOffsetString(value.value);
|
|
60
|
+
}
|
|
61
|
+
return 'unknown';
|
|
62
|
+
}
|
|
63
|
+
// Negative numeric literals parse as `-` UnaryExpression over a number.
|
|
64
|
+
if (value.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
|
65
|
+
value.operator === '-' &&
|
|
66
|
+
value.argument.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
67
|
+
typeof value.argument.value === 'number') {
|
|
68
|
+
return value.argument.value === 0 ? 'zero' : 'negative';
|
|
69
|
+
}
|
|
70
|
+
return 'unknown';
|
|
71
|
+
}
|
|
26
72
|
function formatSelector(selector) {
|
|
27
73
|
if (!selector)
|
|
28
74
|
return 'pseudo-element';
|
|
@@ -63,12 +109,21 @@ exports.ensurePointerEventsNone = (0, createRule_1.createRule)({
|
|
|
63
109
|
const absolutePositionedStyles = new Map();
|
|
64
110
|
// Track style objects that already have pointer-events defined
|
|
65
111
|
const stylesWithPointerEvents = new Map();
|
|
112
|
+
// Track style objects that are hit-slop touch-target extensions: an
|
|
113
|
+
// absolute/fixed overlay whose inset offsets only extend beyond the origin
|
|
114
|
+
// box (>=1 negative, none positive). A browser attributes pointer events on
|
|
115
|
+
// a pseudo-element to its origin element, so such an overlay cannot occlude
|
|
116
|
+
// the control and must not be flagged (its autofix would shrink the tap
|
|
117
|
+
// target, the very accessibility regression this rule exists to prevent).
|
|
118
|
+
const hitSlopStyles = new Map();
|
|
66
119
|
/**
|
|
67
120
|
* Process a CSS-in-JS style object to check for position: absolute/fixed and pointer-events
|
|
68
121
|
*/
|
|
69
122
|
function processStyleObject(node) {
|
|
70
123
|
let hasAbsolutePosition = false;
|
|
71
124
|
let pointerEventsValue;
|
|
125
|
+
let hasNegativeOffset = false;
|
|
126
|
+
let hasPositiveOffset = false;
|
|
72
127
|
// Check each property in the style object
|
|
73
128
|
for (const property of node.properties) {
|
|
74
129
|
if (property.type !== utils_1.AST_NODE_TYPES.Property)
|
|
@@ -103,12 +158,26 @@ exports.ensurePointerEventsNone = (0, createRule_1.createRule)({
|
|
|
103
158
|
pointerEventsValue = property.value.name;
|
|
104
159
|
}
|
|
105
160
|
}
|
|
161
|
+
// Track inset offsets to detect hit-slop touch-target extensions
|
|
162
|
+
if (INSET_PROPERTIES.has(propertyName)) {
|
|
163
|
+
const sign = classifyOffsetValue(property.value);
|
|
164
|
+
if (sign === 'negative') {
|
|
165
|
+
hasNegativeOffset = true;
|
|
166
|
+
}
|
|
167
|
+
else if (sign === 'positive') {
|
|
168
|
+
hasPositiveOffset = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
106
171
|
}
|
|
107
172
|
// Store the results for this style object
|
|
108
173
|
absolutePositionedStyles.set(node, hasAbsolutePosition);
|
|
109
174
|
if (pointerEventsValue !== undefined) {
|
|
110
175
|
stylesWithPointerEvents.set(node, pointerEventsValue);
|
|
111
176
|
}
|
|
177
|
+
// A hit-slop extension only enlarges the tappable area: it is
|
|
178
|
+
// absolute/fixed and its inset offsets extend outward (>=1 negative, none
|
|
179
|
+
// positive). Such overlays cannot occlude the control they belong to.
|
|
180
|
+
hitSlopStyles.set(node, hasAbsolutePosition && hasNegativeOffset && !hasPositiveOffset);
|
|
112
181
|
}
|
|
113
182
|
/**
|
|
114
183
|
* Check if a style object needs pointer-events: none
|
|
@@ -117,6 +186,13 @@ exports.ensurePointerEventsNone = (0, createRule_1.createRule)({
|
|
|
117
186
|
const isPseudoElement = selector && hasPseudoElementSelector(selector);
|
|
118
187
|
const isAbsolutePositioned = absolutePositionedStyles.get(node) || false;
|
|
119
188
|
const pointerEventsValue = stylesWithPointerEvents.get(node);
|
|
189
|
+
// A hit-slop touch-target extension extends the origin element's tappable
|
|
190
|
+
// area outward; because pointer events on it are attributed to the origin
|
|
191
|
+
// control, it cannot block anything. Skip reporting (and its destructive
|
|
192
|
+
// shrink-the-tap-target autofix).
|
|
193
|
+
if (hitSlopStyles.get(node)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
120
196
|
// If this is a pseudo-element with absolute positioning but no pointer-events
|
|
121
197
|
if (isPseudoElement &&
|
|
122
198
|
isAbsolutePositioned &&
|
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
type Options = [
|
|
2
|
+
{
|
|
3
|
+
additionalSubjectExtensions?: string[];
|
|
4
|
+
}?
|
|
5
|
+
];
|
|
6
|
+
export declare const testFileLocationEnforcement: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"misplacedTestFile", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
7
|
+
export {};
|
|
@@ -9,6 +9,10 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const createRule_1 = require("../utils/createRule");
|
|
10
10
|
const TEST_FILE_PATTERN = /\.test\.tsx?$/i;
|
|
11
11
|
const SUPPORTED_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
|
|
12
|
+
const DEFAULT_OPTIONS = {
|
|
13
|
+
additionalSubjectExtensions: [],
|
|
14
|
+
};
|
|
15
|
+
const normalizeExtension = (extension) => extension.startsWith('.') ? extension : `.${extension}`;
|
|
12
16
|
exports.testFileLocationEnforcement = (0, createRule_1.createRule)({
|
|
13
17
|
name: 'test-file-location-enforcement',
|
|
14
18
|
meta: {
|
|
@@ -17,13 +21,31 @@ exports.testFileLocationEnforcement = (0, createRule_1.createRule)({
|
|
|
17
21
|
description: 'Enforce colocating *.test.ts or *.test.tsx files with the code they cover.',
|
|
18
22
|
recommended: 'error',
|
|
19
23
|
},
|
|
20
|
-
schema: [
|
|
24
|
+
schema: [
|
|
25
|
+
{
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
additionalSubjectExtensions: {
|
|
29
|
+
type: 'array',
|
|
30
|
+
items: { type: 'string' },
|
|
31
|
+
default: [],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
21
37
|
messages: {
|
|
22
38
|
misplacedTestFile: 'Test file "{{testFile}}" is not colocated with its subject. Keep tests in the same directory as {{expectedNames}} so refactors move code and coverage together and engineers can find the implementation without searching separate test folders.',
|
|
23
39
|
},
|
|
24
40
|
},
|
|
25
|
-
defaultOptions: [],
|
|
26
|
-
create(context) {
|
|
41
|
+
defaultOptions: [DEFAULT_OPTIONS],
|
|
42
|
+
create(context, [options]) {
|
|
43
|
+
const resolvedOptions = { ...DEFAULT_OPTIONS, ...(options ?? {}) };
|
|
44
|
+
const additionalExtensions = (resolvedOptions.additionalSubjectExtensions ?? []).map(normalizeExtension);
|
|
45
|
+
const subjectExtensions = [
|
|
46
|
+
...SUPPORTED_EXTENSIONS,
|
|
47
|
+
...additionalExtensions.filter((extension) => !SUPPORTED_EXTENSIONS.includes(extension)),
|
|
48
|
+
];
|
|
27
49
|
return {
|
|
28
50
|
Program(node) {
|
|
29
51
|
const filename = context.getFilename();
|
|
@@ -36,7 +58,7 @@ exports.testFileLocationEnforcement = (0, createRule_1.createRule)({
|
|
|
36
58
|
const directory = path_1.default.dirname(filename);
|
|
37
59
|
const testFileName = path_1.default.basename(filename);
|
|
38
60
|
const baseName = testFileName.replace(TEST_FILE_PATTERN, '');
|
|
39
|
-
const candidates =
|
|
61
|
+
const candidates = subjectExtensions.map((extension) => path_1.default.join(directory, `${baseName}${extension}`));
|
|
40
62
|
const hasSibling = candidates.some((candidate) => fs_1.default.existsSync(candidate));
|
|
41
63
|
if (hasSibling) {
|
|
42
64
|
return;
|
|
@@ -44,7 +66,9 @@ exports.testFileLocationEnforcement = (0, createRule_1.createRule)({
|
|
|
44
66
|
const relativePath = path_1.default.isAbsolute(filename)
|
|
45
67
|
? path_1.default.relative(process.cwd(), filename) || filename
|
|
46
68
|
: filename;
|
|
47
|
-
const expectedNames =
|
|
69
|
+
const expectedNames = subjectExtensions
|
|
70
|
+
.map((extension) => `"${baseName}${extension}"`)
|
|
71
|
+
.join(' or ');
|
|
48
72
|
context.report({
|
|
49
73
|
node,
|
|
50
74
|
messageId: 'misplacedTestFile',
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.19.13",
|
|
4
|
+
"date": "2026-07-17T21:25:11.513Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "ensure-pointer-events-none",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1315
|
|
11
|
+
],
|
|
12
|
+
"summary": "exempt hit-slop touch-target pseudo-elements (closes #1315)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.19.12",
|
|
18
|
+
"date": "2026-07-17T19:23:55.157Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "test-file-location-enforcement",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1314
|
|
25
|
+
],
|
|
26
|
+
"summary": "add opt-in additionalSubjectExtensions for non-JS/TS subjects (closes #1314)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.19.11",
|
|
4
32
|
"date": "2026-07-17T10:46:20.593Z",
|