check-custom-rules.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. #!/usr/bin/env node
  2. /**
  3. * Licensed to the Apache Software Foundation (ASF) under one
  4. * or more contributor license agreements. See the NOTICE file
  5. * distributed with this work for additional information
  6. * regarding copyright ownership. The ASF licenses this file
  7. * to you under the Apache License, Version 2.0 (the
  8. * "License"); you may not use this file except in compliance
  9. * with the License. You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing,
  14. * software distributed under the License is distributed on an
  15. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  16. * KIND, either express or implied. See the License for the
  17. * specific language governing permissions and limitations
  18. * under the License.
  19. */
  20. /**
  21. * Custom rule checker for Superset-specific linting patterns
  22. * Runs as a separate check without needing custom binaries
  23. */
  24. const fs = require('fs');
  25. const path = require('path');
  26. const glob = require('glob');
  27. const parser = require('@babel/parser');
  28. const traverse = require('@babel/traverse').default;
  29. // ANSI color codes
  30. const RED = '\x1b[31m';
  31. const YELLOW = '\x1b[33m';
  32. const RESET = '\x1b[0m';
  33. let errorCount = 0;
  34. let warningCount = 0;
  35. /**
  36. * Check if a node has an eslint-disable comment
  37. */
  38. function hasEslintDisable(path, ruleName = 'theme-colors/no-literal-colors') {
  39. const { node, parent } = path;
  40. // Check leadingComments on the node itself
  41. if (node.leadingComments) {
  42. const hasDisable = node.leadingComments.some(
  43. comment =>
  44. (comment.value.includes('eslint-disable-next-line') ||
  45. comment.value.includes('eslint-disable')) &&
  46. comment.value.includes(ruleName),
  47. );
  48. if (hasDisable) return true;
  49. }
  50. // Check leadingComments on parent nodes (for expressions in assignments, etc.)
  51. if (parent && parent.leadingComments) {
  52. const hasDisable = parent.leadingComments.some(
  53. comment =>
  54. (comment.value.includes('eslint-disable-next-line') ||
  55. comment.value.includes('eslint-disable')) &&
  56. comment.value.includes(ruleName),
  57. );
  58. if (hasDisable) return true;
  59. }
  60. // Check if parent is a statement with leading comments
  61. let current = path;
  62. while (current.parent) {
  63. current = current.parent;
  64. if (current.node && current.node.leadingComments) {
  65. const hasDisable = current.node.leadingComments.some(
  66. comment =>
  67. (comment.value.includes('eslint-disable-next-line') ||
  68. comment.value.includes('eslint-disable')) &&
  69. comment.value.includes(ruleName),
  70. );
  71. if (hasDisable) return true;
  72. }
  73. }
  74. return false;
  75. }
  76. /**
  77. * Check for literal color values (hex, rgb, rgba)
  78. */
  79. function checkNoLiteralColors(ast, filepath) {
  80. const colorPatterns = [
  81. /^#[0-9A-Fa-f]{3,6}$/, // Hex colors
  82. /^rgb\(/, // RGB colors
  83. /^rgba\(/, // RGBA colors
  84. ];
  85. traverse(ast, {
  86. StringLiteral(path) {
  87. const { value } = path.node;
  88. if (colorPatterns.some(pattern => pattern.test(value))) {
  89. // Check if this line has an eslint-disable comment
  90. if (hasEslintDisable(path)) {
  91. return; // Skip this violation
  92. }
  93. // eslint-disable-next-line no-console
  94. console.error(
  95. `${RED}✖${RESET} ${filepath}: Literal color "${value}" found. Use theme colors instead.`,
  96. );
  97. errorCount += 1;
  98. }
  99. },
  100. // Check styled-components template literals
  101. TemplateLiteral(path) {
  102. path.node.quasis.forEach(quasi => {
  103. const value = quasi.value.raw;
  104. // Look for CSS color properties
  105. if (
  106. value.match(
  107. /(?:color|background|border-color|outline-color):\s*(#[0-9A-Fa-f]{3,6}|rgb|rgba)/,
  108. )
  109. ) {
  110. // Check if this line has an eslint-disable comment
  111. if (hasEslintDisable(path)) {
  112. return; // Skip this violation
  113. }
  114. // eslint-disable-next-line no-console
  115. console.error(
  116. `${RED}✖${RESET} ${filepath}: Literal color in styled component. Use theme colors instead.`,
  117. );
  118. errorCount += 1;
  119. }
  120. });
  121. },
  122. });
  123. }
  124. /**
  125. * Check for FontAwesome icon usage
  126. */
  127. function checkNoFaIcons(ast, filepath) {
  128. traverse(ast, {
  129. ImportDeclaration(path) {
  130. const source = path.node.source.value;
  131. if (source.includes('@fortawesome') || source.includes('font-awesome')) {
  132. // eslint-disable-next-line no-console
  133. console.error(
  134. `${RED}✖${RESET} ${filepath}: FontAwesome import detected. Use @superset-ui/core/components/Icons instead.`,
  135. );
  136. errorCount += 1;
  137. }
  138. },
  139. JSXAttribute(path) {
  140. if (path.node.name.name === 'className') {
  141. const { value } = path.node;
  142. if (
  143. value &&
  144. value.type === 'StringLiteral' &&
  145. value.value.includes('fa-')
  146. ) {
  147. // eslint-disable-next-line no-console
  148. console.error(
  149. `${RED}✖${RESET} ${filepath}: FontAwesome class detected. Use Icons component instead.`,
  150. );
  151. errorCount += 1;
  152. }
  153. }
  154. },
  155. });
  156. }
  157. /**
  158. * Check for improper i18n template usage
  159. */
  160. function checkI18nTemplates(ast, filepath) {
  161. traverse(ast, {
  162. CallExpression(path) {
  163. const { callee } = path.node;
  164. // Check for t() or tn() functions
  165. if (
  166. callee.type === 'Identifier' &&
  167. (callee.name === 't' || callee.name === 'tn')
  168. ) {
  169. const args = path.node.arguments;
  170. if (args.length > 0 && args[0].type === 'TemplateLiteral') {
  171. const templateLiteral = args[0];
  172. if (templateLiteral.expressions.length > 0) {
  173. // eslint-disable-next-line no-console
  174. console.error(
  175. `${RED}✖${RESET} ${filepath}: Template variables in t() function. Use parameterized messages instead.`,
  176. );
  177. errorCount += 1;
  178. }
  179. }
  180. }
  181. },
  182. });
  183. }
  184. /**
  185. * Process a single file
  186. */
  187. function processFile(filepath) {
  188. const code = fs.readFileSync(filepath, 'utf8');
  189. try {
  190. const ast = parser.parse(code, {
  191. sourceType: 'module',
  192. plugins: ['jsx', 'typescript', 'decorators-legacy'],
  193. attachComments: true,
  194. });
  195. // Run all checks
  196. checkNoLiteralColors(ast, filepath);
  197. checkNoFaIcons(ast, filepath);
  198. checkI18nTemplates(ast, filepath);
  199. } catch (error) {
  200. // eslint-disable-next-line no-console
  201. console.warn(
  202. `${YELLOW}⚠${RESET} Could not parse ${filepath}: ${error.message}`,
  203. );
  204. warningCount += 1;
  205. }
  206. }
  207. /**
  208. * Main function
  209. */
  210. function main() {
  211. const args = process.argv.slice(2);
  212. let files = args;
  213. // Define ignore patterns once
  214. const ignorePatterns = [
  215. /\.test\./,
  216. /\.spec\./,
  217. /\/test\//,
  218. /\/tests\//,
  219. /\/storybook\//,
  220. /\.stories\./,
  221. /\/demo\//,
  222. /\/examples\//,
  223. /\/color\/colorSchemes\//,
  224. /\/cypress\//,
  225. /\/cypress-base\//,
  226. /packages\/superset-ui-demo\//,
  227. /\/esm\//,
  228. /\/lib\//,
  229. /\/dist\//,
  230. /plugins\/legacy-/, // Legacy plugins can have old color patterns
  231. /\/vendor\//, // Third-party vendor code
  232. /spec\/fixtures\//, // Test fixtures
  233. /theme\/exampleThemes/, // Theme examples legitimately have colors
  234. /\/color\/utils/, // Color utility functions legitimately work with colors
  235. /\/theme\/utils/, // Theme utility functions legitimately work with colors
  236. /packages\/superset-ui-core\/src\/color\/index\.ts/, // Core brand color constants
  237. ];
  238. // If no files specified, check all
  239. if (files.length === 0) {
  240. files = glob.sync('src/**/*.{ts,tsx,js,jsx}', {
  241. ignore: [
  242. '**/*.test.*',
  243. '**/*.spec.*',
  244. '**/test/**',
  245. '**/tests/**',
  246. '**/node_modules/**',
  247. '**/storybook/**',
  248. '**/*.stories.*',
  249. '**/demo/**',
  250. '**/examples/**',
  251. '**/color/colorSchemes/**', // Color scheme definitions legitimately contain colors
  252. '**/cypress/**',
  253. '**/cypress-base/**',
  254. 'packages/superset-ui-demo/**', // Demo package
  255. '**/esm/**', // Build artifacts
  256. '**/lib/**', // Build artifacts
  257. '**/dist/**', // Build artifacts
  258. 'plugins/legacy-*/**', // Legacy plugins
  259. '**/vendor/**',
  260. 'spec/fixtures/**',
  261. '**/theme/exampleThemes/**',
  262. '**/color/utils/**',
  263. '**/theme/utils/**',
  264. 'packages/superset-ui-core/src/color/index.ts', // Core brand color constants
  265. ],
  266. });
  267. } else {
  268. // Filter to only JS/TS files and remove superset-frontend prefix
  269. files = files
  270. .filter(f => /\.(ts|tsx|js|jsx)$/.test(f))
  271. .map(f => f.replace(/^superset-frontend\//, ''))
  272. .filter(f => !ignorePatterns.some(pattern => pattern.test(f)));
  273. }
  274. if (files.length === 0) {
  275. // eslint-disable-next-line no-console
  276. console.log('No files to check.');
  277. return;
  278. }
  279. // eslint-disable-next-line no-console
  280. console.log(`Checking ${files.length} files for Superset custom rules...\\n`);
  281. files.forEach(file => {
  282. // Resolve the file path
  283. const resolvedPath = path.resolve(file);
  284. if (fs.existsSync(resolvedPath)) {
  285. processFile(resolvedPath);
  286. } else if (fs.existsSync(file)) {
  287. processFile(file);
  288. }
  289. });
  290. // eslint-disable-next-line no-console
  291. console.log(`\\n${errorCount} errors, ${warningCount} warnings`);
  292. if (errorCount > 0) {
  293. process.exit(1);
  294. }
  295. }
  296. // Run if called directly
  297. if (require.main === module) {
  298. main();
  299. }
  300. module.exports = { checkNoLiteralColors, checkNoFaIcons, checkI18nTemplates };