| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- #!/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 };
|