@risemaxi/syntactio
Version:
Linting and formatting config for Rise client apps. Supports ESLint, Oxlint, and Oxfmt.
360 lines (359 loc) • 10.3 kB
JavaScript
;
/**
* oxlint-compatible rewrite of eslint-plugin-react-native rules.
*
* Uses oxlint's createOnce API for better performance.
* The original plugin's Components.detect() wrapper uses context.getScope()
* (ESLint v8 API) which crashes under oxlint's JS Plugin API.
*
* Rules reimplemented:
* - react-native/no-unused-styles
* - react-native/no-inline-styles
* - react-native/no-color-literals
*/
class StyleSheets {
constructor() {
this.styleSheets = {};
}
add(name, props) {
this.styleSheets[name] = props;
}
markAsUsed(ref) {
const [sheet, prop] = ref.split(".");
if (this.styleSheets[sheet]) {
this.styleSheets[sheet] = this.styleSheets[sheet].filter((p) => p.key?.name !== prop);
}
}
getUnusedReferences() {
return this.styleSheets;
}
}
const COLOR_RE = /color/i;
const HEX_RE = /^#([0-9a-f]{3,8})$/i;
const RGB_RE = /^(rgba?|hsla?)\s*\(/i;
const NAMED_COLORS = new Set([
"aliceblue",
"antiquewhite",
"aqua",
"aquamarine",
"azure",
"beige",
"bisque",
"black",
"blanchedalmond",
"blue",
"blueviolet",
"brown",
"burlywood",
"cadetblue",
"chartreuse",
"chocolate",
"coral",
"cornflowerblue",
"cornsilk",
"crimson",
"cyan",
"darkblue",
"darkcyan",
"darkgoldenrod",
"darkgray",
"darkgreen",
"darkgrey",
"darkkhaki",
"darkmagenta",
"darkolivegreen",
"darkorange",
"darkorchid",
"darkred",
"darksalmon",
"darkseagreen",
"darkslateblue",
"darkslategray",
"darkslategrey",
"darkturquoise",
"darkviolet",
"deeppink",
"deepskyblue",
"dimgray",
"dimgrey",
"dodgerblue",
"firebrick",
"floralwhite",
"forestgreen",
"fuchsia",
"gainsboro",
"ghostwhite",
"gold",
"goldenrod",
"gray",
"green",
"greenyellow",
"grey",
"honeydew",
"hotpink",
"indianred",
"indigo",
"ivory",
"khaki",
"lavender",
"lavenderblush",
"lawngreen",
"lemonchiffon",
"lightblue",
"lightcoral",
"lightcyan",
"lightgoldenrodyellow",
"lightgray",
"lightgreen",
"lightgrey",
"lightpink",
"lightsalmon",
"lightseagreen",
"lightskyblue",
"lightslategray",
"lightslategrey",
"lightsteelblue",
"lightyellow",
"lime",
"limegreen",
"linen",
"magenta",
"maroon",
"mediumaquamarine",
"mediumblue",
"mediumorchid",
"mediumpurple",
"mediumseagreen",
"mediumslateblue",
"mediumspringgreen",
"mediumturquoise",
"mediumvioletred",
"midnightblue",
"mintcream",
"mistyrose",
"moccasin",
"navajowhite",
"navy",
"oldlace",
"olive",
"olivedrab",
"orange",
"orangered",
"orchid",
"palegoldenrod",
"palegreen",
"paleturquoise",
"palevioletred",
"papayawhip",
"peachpuff",
"peru",
"pink",
"plum",
"powderblue",
"purple",
"rebeccapurple",
"red",
"rosybrown",
"royalblue",
"saddlebrown",
"salmon",
"sandybrown",
"seagreen",
"seashell",
"sienna",
"silver",
"skyblue",
"slateblue",
"slategray",
"slategrey",
"snow",
"springgreen",
"steelblue",
"tan",
"teal",
"thistle",
"tomato",
"turquoise",
"violet",
"wheat",
"white",
"whitesmoke",
"yellow",
"yellowgreen",
]);
function isStyleAttribute(node) {
return node.type === "JSXAttribute" && node.name?.name?.toLowerCase().includes("style");
}
function isStyleSheetDeclaration(node, settings) {
const names = settings?.["react-native/style-sheet-object-names"] ?? ["StyleSheet"];
return (node?.type === "CallExpression" &&
node.callee?.object &&
names.includes(node.callee.object.name) &&
node.callee.property?.name === "create");
}
function getStyleSheetName(node) {
return node?.parent?.id?.name;
}
function getStyleDeclarations(node) {
if (node?.type === "CallExpression" && node.arguments?.[0]?.properties) {
return node.arguments[0].properties.filter((p) => p.type === "Property");
}
return [];
}
function getPotentialStyleRef(node) {
if (node?.object?.type === "Identifier" &&
node.object.name &&
node.property?.type === "Identifier" &&
node.property.name &&
node.parent.type !== "MemberExpression") {
return `${node.object.name}.${node.property.name}`;
}
}
function collectObjectExpressions(node) {
if (!node)
return [];
if (node.type === "JSXExpressionContainer" && node.expression) {
return collectObjectExpressions(node.expression);
}
if (node.type === "ArrayExpression") {
return node.elements.flatMap(collectObjectExpressions);
}
if (node.type === "ObjectExpression") {
const hasLiteral = node.properties.some((p) => p.value &&
(p.value.type === "Literal" || (p.value.type === "UnaryExpression" && p.value.argument?.type === "Literal")));
return hasLiteral ? [{ expression: node, node }] : [];
}
return [];
}
function isColorValue(value) {
if (typeof value !== "string")
return false;
return HEX_RE.test(value) || RGB_RE.test(value) || NAMED_COLORS.has(value.toLowerCase());
}
function collectColorLiterals(node) {
if (!node)
return [];
if (node.type === "JSXExpressionContainer" && node.expression) {
return collectColorLiterals(node.expression);
}
if (node.type === "ArrayExpression") {
return node.elements.flatMap(collectColorLiterals);
}
if (node.type !== "ObjectExpression")
return [];
const found = {};
let hasColor = false;
for (const p of node.properties) {
if (!p.key || !p.value)
continue;
const keyName = p.key.name ?? (p.key.value && String(p.key.value));
if (!keyName || !COLOR_RE.test(keyName))
continue;
if (p.value.type === "Literal" && isColorValue(p.value.value)) {
found[keyName] = p.value.value;
hasColor = true;
}
}
return hasColor ? [{ expression: found, node }] : [];
}
module.exports = {
meta: {
name: "react-native-compat",
},
rules: {
"no-unused-styles": {
createOnce(context) {
let sheets;
let refs;
return {
Program() {
sheets = new StyleSheets();
refs = new Set();
},
MemberExpression(node) {
const ref = getPotentialStyleRef(node);
if (ref)
refs.add(ref);
},
CallExpression(node) {
if (isStyleSheetDeclaration(node, context.settings)) {
sheets.add(getStyleSheetName(node), getStyleDeclarations(node));
}
},
"Program:exit"() {
refs.forEach((r) => sheets.markAsUsed(r));
const unused = sheets.getUnusedReferences();
for (const [sheetName, props] of Object.entries(unused)) {
for (const prop of props) {
context.report({
node: prop,
message: "Unused style detected: {{sheet}}.{{prop}}",
data: { sheet: sheetName, prop: prop.key.name },
});
}
}
},
};
},
},
"no-inline-styles": {
createOnce(context) {
let expressions;
return {
Program() {
expressions = [];
},
JSXAttribute(node) {
if (isStyleAttribute(node)) {
expressions = expressions.concat(collectObjectExpressions(node.value));
}
},
"Program:exit"() {
for (const style of expressions) {
if (style) {
context.report({
node: style.node,
message: "Inline style detected",
});
}
}
},
};
},
},
"no-color-literals": {
createOnce(context) {
let literals;
return {
Program() {
literals = [];
},
CallExpression(node) {
if (isStyleSheetDeclaration(node, context.settings)) {
for (const style of getStyleDeclarations(node)) {
literals = literals.concat(collectColorLiterals(style.value));
}
}
},
JSXAttribute(node) {
if (isStyleAttribute(node)) {
literals = literals.concat(collectColorLiterals(node.value));
}
},
"Program:exit"() {
for (const lit of literals) {
if (lit) {
const keys = Object.keys(lit.expression);
context.report({
node: lit.node,
message: "Color literal in style: use a constant instead of {{keys}}",
data: { keys: keys.join(", ") },
});
}
}
},
};
},
},
},
};