UNPKG

@risemaxi/syntactio

Version:

Linting and formatting config for Rise client apps. Supports ESLint, Oxlint, and Oxfmt.

360 lines (359 loc) 10.3 kB
"use strict"; /** * 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(", ") }, }); } } }, }; }, }, }, };