index.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. // @ts-nocheck
  2. 'use strict';
  3. const _ = require('lodash');
  4. const beforeBlockString = require('../../utils/beforeBlockString');
  5. const blurComments = require('../../utils/blurComments');
  6. const hasBlock = require('../../utils/hasBlock');
  7. const isCustomProperty = require('../../utils/isCustomProperty');
  8. const isLessVariable = require('../../utils/isLessVariable');
  9. const isMathFunction = require('../../utils/isMathFunction');
  10. const keywordSets = require('../../reference/keywordSets');
  11. const optionsMatches = require('../../utils/optionsMatches');
  12. const report = require('../../utils/report');
  13. const ruleMessages = require('../../utils/ruleMessages');
  14. const styleSearch = require('style-search');
  15. const validateOptions = require('../../utils/validateOptions');
  16. const valueParser = require('postcss-value-parser');
  17. const ruleName = 'length-zero-no-unit';
  18. const messages = ruleMessages(ruleName, {
  19. rejected: 'Unexpected unit',
  20. });
  21. function rule(actual, secondary, context) {
  22. return (root, result) => {
  23. const validOptions = validateOptions(result, ruleName, { actual });
  24. if (!validOptions) {
  25. return;
  26. }
  27. root.walkDecls((decl) => {
  28. if (decl.prop.toLowerCase() === 'line-height') {
  29. return;
  30. }
  31. const stringValue = blurComments(decl.toString());
  32. const ignorableIndexes = new Array(stringValue.length).fill(false);
  33. const parsedValue = valueParser(stringValue);
  34. parsedValue.walk((node, nodeIndex, nodes) => {
  35. if (decl.prop.toLowerCase() === 'font' && node.type === 'div' && node.value === '/') {
  36. const lineHeightNode = nodes[nodeIndex + 1];
  37. const lineHeightNodeValue = valueParser.stringify(lineHeightNode);
  38. for (let i = 0; i < lineHeightNodeValue.length; i++) {
  39. ignorableIndexes[lineHeightNode.sourceIndex + i] = true;
  40. }
  41. return;
  42. }
  43. if (node.type !== 'function') {
  44. return;
  45. }
  46. const stringValue = valueParser.stringify(node);
  47. const ignoreFlag = isMathFunction(node);
  48. for (let i = 0; i < stringValue.length; i++) {
  49. ignorableIndexes[node.sourceIndex + i] = ignoreFlag;
  50. }
  51. });
  52. check(stringValue, decl, ignorableIndexes);
  53. });
  54. root.walkAtRules((atRule) => {
  55. // Ignore Less variables
  56. if (isLessVariable(atRule)) {
  57. return;
  58. }
  59. const source = hasBlock(atRule)
  60. ? beforeBlockString(atRule, { noRawBefore: true })
  61. : atRule.toString();
  62. check(source, atRule);
  63. });
  64. function check(value, node, ignorableIndexes = []) {
  65. if (optionsMatches(secondary, 'ignore', 'custom-properties') && isCustomProperty(value)) {
  66. return;
  67. }
  68. const fixPositions = [];
  69. styleSearch({ source: value, target: '0' }, (match) => {
  70. const index = match.startIndex;
  71. // Given a 0 somewhere in the full property value (not in a string, thanks
  72. // to styleSearch) we need to isolate the value that contains the zero.
  73. // To do so, we'll find the last index before the 0 of a character that would
  74. // divide one value in a list from another, and the next index of such a
  75. // character; then we build a substring from those indexes, which we can
  76. // assess.
  77. // If a single value includes multiple 0's (e.g. 100.01px), we don't want
  78. // each 0 to be treated as a separate value, possibly resulting in multiple
  79. // warnings for the same value (e.g. 0.00px).
  80. //
  81. // This check prevents that from happening: we build and check against a
  82. // Set containing all the indexes that are part of a value already validated.
  83. if (ignorableIndexes[index]) {
  84. return;
  85. }
  86. const prevValueBreakIndex = _.findLastIndex(value.substr(0, index), (char) => {
  87. return [' ', ',', ')', '(', '#', ':', '\n', '\t'].includes(char);
  88. });
  89. // Ignore hex colors
  90. if (value[prevValueBreakIndex] === '#') {
  91. return;
  92. }
  93. // If no prev break was found, this value starts at 0
  94. const valueWithZeroStart = prevValueBreakIndex === -1 ? 0 : prevValueBreakIndex + 1;
  95. const nextValueBreakIndex = _.findIndex(value.substr(valueWithZeroStart), (char) => {
  96. return [' ', ',', ')', '/'].includes(char);
  97. });
  98. // If no next break was found, this value ends at the end of the string
  99. const valueWithZeroEnd =
  100. nextValueBreakIndex === -1 ? value.length : nextValueBreakIndex + valueWithZeroStart;
  101. const valueWithZero = value.slice(valueWithZeroStart, valueWithZeroEnd);
  102. const parsedValue = valueParser.unit(valueWithZero);
  103. if (!parsedValue || (parsedValue && !parsedValue.unit)) {
  104. return;
  105. }
  106. if (parsedValue.unit.toLowerCase() === 'fr') {
  107. return;
  108. }
  109. // Add the indexes to ignorableIndexes so the same value will not
  110. // be checked multiple times.
  111. _.range(valueWithZeroStart, valueWithZeroEnd).forEach((i) => (ignorableIndexes[i] = true));
  112. // Only pay attention if the value parses to 0
  113. // and units with lengths
  114. if (
  115. parseFloat(valueWithZero) !== 0 ||
  116. !keywordSets.lengthUnits.has(parsedValue.unit.toLowerCase())
  117. ) {
  118. return;
  119. }
  120. if (context.fix) {
  121. fixPositions.unshift({
  122. startIndex: valueWithZeroStart,
  123. length: valueWithZeroEnd - valueWithZeroStart,
  124. });
  125. return;
  126. }
  127. report({
  128. message: messages.rejected,
  129. node,
  130. index: valueWithZeroEnd - parsedValue.unit.length,
  131. result,
  132. ruleName,
  133. });
  134. });
  135. if (fixPositions.length) {
  136. fixPositions.forEach((fixPosition) => {
  137. if (node.type === 'atrule') {
  138. // Use `-1` for `@` character before each at rule
  139. const realIndex =
  140. fixPosition.startIndex - node.name.length - node.raws.afterName.length - 1;
  141. node.params = replaceZero(node.params, realIndex, fixPosition.length);
  142. } else {
  143. const realIndex = fixPosition.startIndex - node.prop.length - node.raws.between.length;
  144. node.value = replaceZero(node.value, realIndex, fixPosition.length);
  145. }
  146. });
  147. }
  148. }
  149. };
  150. }
  151. function replaceZero(input, startIndex, length) {
  152. const stringStart = input.slice(0, startIndex);
  153. const stringEnd = input.slice(startIndex + length);
  154. return `${stringStart}0${stringEnd}`;
  155. }
  156. rule.ruleName = ruleName;
  157. rule.messages = messages;
  158. module.exports = rule;