index.mjs 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import { createRequire } from 'node:module';
  2. import { isFunctionNode, parseListOfComponentValues, walk } from '@csstools/css-parser-algorithms';
  3. import { tokenize } from '@csstools/css-tokenizer';
  4. import { isRegExp, isString } from '../../utils/validateTypes.mjs';
  5. import { declarationValueIndex } from '../../utils/nodeFieldIndices.mjs';
  6. import isCustomFunction from '../../utils/isCustomFunction.mjs';
  7. import isStandardSyntaxValue from '../../utils/isStandardSyntaxValue.mjs';
  8. import optionsMatches from '../../utils/optionsMatches.mjs';
  9. import report from '../../utils/report.mjs';
  10. import ruleMessages from '../../utils/ruleMessages.mjs';
  11. import validateOptions from '../../utils/validateOptions.mjs';
  12. /** @todo leverage import attributes once support for Node.js v18.19 is dropped */
  13. const require = createRequire(import.meta.url);
  14. const functionsList = require('css-functions-list/index.json');
  15. const FUNCTIONS = [
  16. ...functionsList,
  17. // https://drafts.csswg.org/css-values-5/#if-notation
  18. 'if',
  19. 'media',
  20. 'style',
  21. 'supports',
  22. // niksy/css-functions-list#10
  23. 'content',
  24. 'running',
  25. 'string',
  26. // https://drafts.csswg.org/css-values-5/#tree-counting
  27. 'sibling-count',
  28. 'sibling-index',
  29. // https://drafts.csswg.org/css-color-5/#contrast-color
  30. 'contrast-color',
  31. ];
  32. const ruleName = 'function-no-unknown';
  33. const messages = ruleMessages(ruleName, {
  34. rejected: (name) => `Unexpected unknown function "${name}"`,
  35. });
  36. const meta = {
  37. url: 'https://stylelint.io/user-guide/rules/function-no-unknown',
  38. };
  39. /** @type {import('stylelint').CoreRules[ruleName]} */
  40. const rule = (primary, secondaryOptions) => {
  41. return (root, result) => {
  42. const validOptions = validateOptions(
  43. result,
  44. ruleName,
  45. { actual: primary },
  46. {
  47. actual: secondaryOptions,
  48. possible: {
  49. ignoreFunctions: [isString, isRegExp],
  50. },
  51. optional: true,
  52. },
  53. );
  54. if (!validOptions) {
  55. return;
  56. }
  57. root.walkDecls((decl) => {
  58. const { value } = decl;
  59. if (!value.includes('(')) return;
  60. if (!isStandardSyntaxValue(value)) return;
  61. walk(parseListOfComponentValues(tokenize({ css: value })), ({ node }) => {
  62. if (!isFunctionNode(node)) return;
  63. const name = node.getName();
  64. if (isCustomFunction(name)) return;
  65. if (optionsMatches(secondaryOptions, 'ignoreFunctions', name)) return;
  66. if (FUNCTIONS.includes(name.toLowerCase())) return;
  67. const declIndex = declarationValueIndex(decl);
  68. const index = declIndex + node.name[2];
  69. const endIndex = declIndex + node.name[3];
  70. report({
  71. message: messages.rejected,
  72. messageArgs: [name],
  73. node: decl,
  74. index,
  75. endIndex,
  76. result,
  77. ruleName,
  78. });
  79. });
  80. });
  81. };
  82. };
  83. rule.ruleName = ruleName;
  84. rule.messages = messages;
  85. rule.meta = meta;
  86. export default rule;