mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 02:08:57 +00:00
126 lines
3.9 KiB
JavaScript
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);
|
|
},
|
|
};
|
|
},
|
|
};
|