Files
Desktop/.eslint/rules/enforce-tw.js
2025-08-11 16:46:23 -07:00

126 lines
3.9 KiB
JavaScript

// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
const { createSyncFn } = require('synckit');
const worker = createSyncFn(require.resolve('./enforce-tw.worker.js'));
/** @type {import("eslint").Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
hasSuggestions: true,
fixable: true,
schema: [],
},
create(context) {
function check(input, node) {
if (typeof input !== 'string') {
throw new Error(`Unexpected input ${input} for node type ${node.type}`);
}
const tailwindClasses = worker(input.split(/\s+/));
for (const tailwindClass of tailwindClasses) {
const index = input.indexOf(tailwindClass) + 1;
const length = tailwindClass.length;
context.report({
node,
loc: {
start: {
line: node.loc.start.line,
column: node.loc.start.column + index,
},
end: {
line: node.loc.end.line,
column: node.loc.start.column + index + length,
},
},
message: 'Tailwind classes must be wrapped with tw()',
});
}
}
function traverse(node) {
if (node.type === 'Literal') {
if (typeof node.value === 'string') {
check(node.value, node);
}
// ignore other literals
} else if (node.type === 'TemplateLiteral') {
for (let element of node.quasis) {
traverse(element);
}
for (let expression of node.expressions) {
traverse(expression);
}
} else if (node.type === 'TemplateElement') {
check(node.value.cooked, node);
} else if (node.type === 'JSXExpressionContainer') {
traverse(node.expression);
} else if (node.type === 'ConditionalExpression') {
// ignore node.test
traverse(node.consequent);
traverse(node.alternate);
} else if (node.type === 'LogicalExpression') {
if (node.operator === '||' || node.operator === '??') {
traverse(node.left);
}
traverse(node.right);
} else if (node.type === 'BinaryExpression') {
if (node.operator === '+') {
traverse(node.left);
traverse(node.right);
} else {
throw new Error(`Unexpected binary operator: ${node.operator}`);
}
} else if (node.type === 'ObjectExpression') {
for (let prop of node.properties) {
traverse(prop);
}
} else if (node.type === 'Property') {
if (node.key.type === 'Identifier') {
if (!node.computed) {
check(node.key.name, node.key);
}
// ignore computed
} else if (node.key.type === 'Literal') {
traverse(node.key);
} else if (node.key.type === 'TemplateLiteral') {
traverse(node.key);
} else if (node.key.type === 'CallExpression') {
// ignore
} else {
throw new Error(`Unexpected property key type: ${node.key.type}`);
}
} else if (node.type === 'ArrayExpression') {
for (let element of node.elements) {
traverse(element);
}
} else if (node.type === 'Identifier') {
// ignore
} else if (node.type === 'CallExpression') {
// ignore
} else if (node.type === 'MemberExpression') {
// ignore
} else {
throw new Error(`Unexpected traverse node type: ${node.type}`);
}
}
return {
CallExpression(node) {
if (node.callee.type !== 'Identifier') return;
if (node.callee.name !== 'classNames') return;
for (let arg of node.arguments) {
traverse(arg);
}
},
JSXAttribute(node) {
if (node.name.type !== 'JSXIdentifier') return;
if (node.name.name !== 'className') return;
traverse(node.value);
},
};
},
};