#!/usr/bin/env node /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /** * Custom rule checker for Superset-specific linting patterns * Runs as a separate check without needing custom binaries */ const fs = require('fs'); const path = require('path'); const glob = require('glob'); const parser = require('@babel/parser'); const traverse = require('@babel/traverse').default; // ANSI color codes const RED = '\x1b[31m'; const YELLOW = '\x1b[33m'; const RESET = '\x1b[0m'; let errorCount = 0; let warningCount = 0; /** * Check if a node has an eslint-disable comment */ function hasEslintDisable(path, ruleName = 'theme-colors/no-literal-colors') { const { node, parent } = path; // Check leadingComments on the node itself if (node.leadingComments) { const hasDisable = node.leadingComments.some( comment => (comment.value.includes('eslint-disable-next-line') || comment.value.includes('eslint-disable')) && comment.value.includes(ruleName), ); if (hasDisable) return true; } // Check leadingComments on parent nodes (for expressions in assignments, etc.) if (parent && parent.leadingComments) { const hasDisable = parent.leadingComments.some( comment => (comment.value.includes('eslint-disable-next-line') || comment.value.includes('eslint-disable')) && comment.value.includes(ruleName), ); if (hasDisable) return true; } // Check if parent is a statement with leading comments let current = path; while (current.parent) { current = current.parent; if (current.node && current.node.leadingComments) { const hasDisable = current.node.leadingComments.some( comment => (comment.value.includes('eslint-disable-next-line') || comment.value.includes('eslint-disable')) && comment.value.includes(ruleName), ); if (hasDisable) return true; } } return false; } /** * Check for literal color values (hex, rgb, rgba) */ function checkNoLiteralColors(ast, filepath) { const colorPatterns = [ /^#[0-9A-Fa-f]{3,6}$/, // Hex colors /^rgb\(/, // RGB colors /^rgba\(/, // RGBA colors ]; traverse(ast, { StringLiteral(path) { const { value } = path.node; if (colorPatterns.some(pattern => pattern.test(value))) { // Check if this line has an eslint-disable comment if (hasEslintDisable(path)) { return; // Skip this violation } // eslint-disable-next-line no-console console.error( `${RED}✖${RESET} ${filepath}: Literal color "${value}" found. Use theme colors instead.`, ); errorCount += 1; } }, // Check styled-components template literals TemplateLiteral(path) { path.node.quasis.forEach(quasi => { const value = quasi.value.raw; // Look for CSS color properties if ( value.match( /(?:color|background|border-color|outline-color):\s*(#[0-9A-Fa-f]{3,6}|rgb|rgba)/, ) ) { // Check if this line has an eslint-disable comment if (hasEslintDisable(path)) { return; // Skip this violation } // eslint-disable-next-line no-console console.error( `${RED}✖${RESET} ${filepath}: Literal color in styled component. Use theme colors instead.`, ); errorCount += 1; } }); }, }); } /** * Check for FontAwesome icon usage */ function checkNoFaIcons(ast, filepath) { traverse(ast, { ImportDeclaration(path) { const source = path.node.source.value; if (source.includes('@fortawesome') || source.includes('font-awesome')) { // eslint-disable-next-line no-console console.error( `${RED}✖${RESET} ${filepath}: FontAwesome import detected. Use @superset-ui/core/components/Icons instead.`, ); errorCount += 1; } }, JSXAttribute(path) { if (path.node.name.name === 'className') { const { value } = path.node; if ( value && value.type === 'StringLiteral' && value.value.includes('fa-') ) { // eslint-disable-next-line no-console console.error( `${RED}✖${RESET} ${filepath}: FontAwesome class detected. Use Icons component instead.`, ); errorCount += 1; } } }, }); } /** * Check for improper i18n template usage */ function checkI18nTemplates(ast, filepath) { traverse(ast, { CallExpression(path) { const { callee } = path.node; // Check for t() or tn() functions if ( callee.type === 'Identifier' && (callee.name === 't' || callee.name === 'tn') ) { const args = path.node.arguments; if (args.length > 0 && args[0].type === 'TemplateLiteral') { const templateLiteral = args[0]; if (templateLiteral.expressions.length > 0) { // eslint-disable-next-line no-console console.error( `${RED}✖${RESET} ${filepath}: Template variables in t() function. Use parameterized messages instead.`, ); errorCount += 1; } } } }, }); } /** * Process a single file */ function processFile(filepath) { const code = fs.readFileSync(filepath, 'utf8'); try { const ast = parser.parse(code, { sourceType: 'module', plugins: ['jsx', 'typescript', 'decorators-legacy'], attachComments: true, }); // Run all checks checkNoLiteralColors(ast, filepath); checkNoFaIcons(ast, filepath); checkI18nTemplates(ast, filepath); } catch (error) { // eslint-disable-next-line no-console console.warn( `${YELLOW}⚠${RESET} Could not parse ${filepath}: ${error.message}`, ); warningCount += 1; } } /** * Main function */ function main() { const args = process.argv.slice(2); let files = args; // Define ignore patterns once const ignorePatterns = [ /\.test\./, /\.spec\./, /\/test\//, /\/tests\//, /\/storybook\//, /\.stories\./, /\/demo\//, /\/examples\//, /\/color\/colorSchemes\//, /\/cypress\//, /\/cypress-base\//, /packages\/superset-ui-demo\//, /\/esm\//, /\/lib\//, /\/dist\//, /plugins\/legacy-/, // Legacy plugins can have old color patterns /\/vendor\//, // Third-party vendor code /spec\/fixtures\//, // Test fixtures /theme\/exampleThemes/, // Theme examples legitimately have colors /\/color\/utils/, // Color utility functions legitimately work with colors /\/theme\/utils/, // Theme utility functions legitimately work with colors /packages\/superset-ui-core\/src\/color\/index\.ts/, // Core brand color constants ]; // If no files specified, check all if (files.length === 0) { files = glob.sync('src/**/*.{ts,tsx,js,jsx}', { ignore: [ '**/*.test.*', '**/*.spec.*', '**/test/**', '**/tests/**', '**/node_modules/**', '**/storybook/**', '**/*.stories.*', '**/demo/**', '**/examples/**', '**/color/colorSchemes/**', // Color scheme definitions legitimately contain colors '**/cypress/**', '**/cypress-base/**', 'packages/superset-ui-demo/**', // Demo package '**/esm/**', // Build artifacts '**/lib/**', // Build artifacts '**/dist/**', // Build artifacts 'plugins/legacy-*/**', // Legacy plugins '**/vendor/**', 'spec/fixtures/**', '**/theme/exampleThemes/**', '**/color/utils/**', '**/theme/utils/**', 'packages/superset-ui-core/src/color/index.ts', // Core brand color constants ], }); } else { // Filter to only JS/TS files and remove superset-frontend prefix files = files .filter(f => /\.(ts|tsx|js|jsx)$/.test(f)) .map(f => f.replace(/^superset-frontend\//, '')) .filter(f => !ignorePatterns.some(pattern => pattern.test(f))); } if (files.length === 0) { // eslint-disable-next-line no-console console.log('No files to check.'); return; } // eslint-disable-next-line no-console console.log(`Checking ${files.length} files for Superset custom rules...\\n`); files.forEach(file => { // Resolve the file path const resolvedPath = path.resolve(file); if (fs.existsSync(resolvedPath)) { processFile(resolvedPath); } else if (fs.existsSync(file)) { processFile(file); } }); // eslint-disable-next-line no-console console.log(`\\n${errorCount} errors, ${warningCount} warnings`); if (errorCount > 0) { process.exit(1); } } // Run if called directly if (require.main === module) { main(); } module.exports = { checkNoLiteralColors, checkNoFaIcons, checkI18nTemplates };