index.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. 'use strict';
  2. const beforeBlockString = require('../../utils/beforeBlockString');
  3. const hasBlock = require('../../utils/hasBlock');
  4. const optionsMatches = require('../../utils/optionsMatches');
  5. const report = require('../../utils/report');
  6. const ruleMessages = require('../../utils/ruleMessages');
  7. const styleSearch = require('style-search');
  8. const validateOptions = require('../../utils/validateOptions');
  9. const { isAtRule, isDeclaration, isRoot, isRule } = require('../../utils/typeGuards');
  10. const { isBoolean, isNumber, isString } = require('../../utils/validateTypes');
  11. const ruleName = 'indentation';
  12. const messages = ruleMessages(ruleName, {
  13. expected: (x) => `Expected indentation of ${x}`,
  14. });
  15. const meta = {
  16. url: 'https://stylelint.io/user-guide/rules/list/indentation',
  17. };
  18. /** @type {import('stylelint').Rule} */
  19. const rule = (primary, secondaryOptions = {}, context) => {
  20. return (root, result) => {
  21. const validOptions = validateOptions(
  22. result,
  23. ruleName,
  24. {
  25. actual: primary,
  26. possible: [isNumber, 'tab'],
  27. },
  28. {
  29. actual: secondaryOptions,
  30. possible: {
  31. baseIndentLevel: [isNumber, 'auto'],
  32. except: ['block', 'value', 'param'],
  33. ignore: ['value', 'param', 'inside-parens'],
  34. indentInsideParens: ['twice', 'once-at-root-twice-in-block'],
  35. indentClosingBrace: [isBoolean],
  36. },
  37. optional: true,
  38. },
  39. );
  40. if (!validOptions) {
  41. return;
  42. }
  43. const spaceCount = isNumber(primary) ? primary : null;
  44. const indentChar = spaceCount == null ? '\t' : ' '.repeat(spaceCount);
  45. const warningWord = primary === 'tab' ? 'tab' : 'space';
  46. /** @type {number | 'auto'} */
  47. const baseIndentLevel = secondaryOptions.baseIndentLevel;
  48. /** @type {boolean} */
  49. const indentClosingBrace = secondaryOptions.indentClosingBrace;
  50. /**
  51. * @param {number} level
  52. */
  53. const legibleExpectation = (level) => {
  54. const count = spaceCount == null ? level : level * spaceCount;
  55. const quantifiedWarningWord = count === 1 ? warningWord : `${warningWord}s`;
  56. return `${count} ${quantifiedWarningWord}`;
  57. };
  58. // Cycle through all nodes using walk.
  59. root.walk((node) => {
  60. if (isRoot(node)) {
  61. // Ignore nested template literals root in css-in-js lang
  62. return;
  63. }
  64. const nodeLevel = indentationLevel(node);
  65. // Cut out any * and _ hacks from `before`
  66. const before = (node.raws.before || '').replace(/[*_]$/, '');
  67. const after = typeof node.raws.after === 'string' ? node.raws.after : '';
  68. const parent = node.parent;
  69. if (!parent) throw new Error('A parent node must be present');
  70. const expectedOpeningBraceIndentation = indentChar.repeat(nodeLevel);
  71. // Only inspect the spaces before the node
  72. // if this is the first node in root
  73. // or there is a newline in the `before` string.
  74. // (If there is no newline before a node,
  75. // there is no "indentation" to check.)
  76. const isFirstChild = parent.type === 'root' && parent.first === node;
  77. const lastIndexOfNewline = before.lastIndexOf('\n');
  78. // Inspect whitespace in the `before` string that is
  79. // *after* the *last* newline character,
  80. // because anything besides that is not indentation for this node:
  81. // it is some other kind of separation, checked by some separate rule
  82. if (
  83. (lastIndexOfNewline !== -1 ||
  84. (isFirstChild &&
  85. (!getDocument(parent) ||
  86. (parent.raws.codeBefore && parent.raws.codeBefore.endsWith('\n'))))) &&
  87. before.slice(lastIndexOfNewline + 1) !== expectedOpeningBraceIndentation
  88. ) {
  89. if (context.fix) {
  90. if (isFirstChild && isString(node.raws.before)) {
  91. node.raws.before = node.raws.before.replace(
  92. /^[ \t]*(?=\S|$)/,
  93. expectedOpeningBraceIndentation,
  94. );
  95. }
  96. node.raws.before = fixIndentation(node.raws.before, expectedOpeningBraceIndentation);
  97. } else {
  98. report({
  99. message: messages.expected(legibleExpectation(nodeLevel)),
  100. node,
  101. result,
  102. ruleName,
  103. });
  104. }
  105. }
  106. // Only blocks have the `after` string to check.
  107. // Only inspect `after` strings that start with a newline;
  108. // otherwise there's no indentation involved.
  109. // And check `indentClosingBrace` to see if it should be indented an extra level.
  110. const closingBraceLevel = indentClosingBrace ? nodeLevel + 1 : nodeLevel;
  111. const expectedClosingBraceIndentation = indentChar.repeat(closingBraceLevel);
  112. if (
  113. (isRule(node) || isAtRule(node)) &&
  114. hasBlock(node) &&
  115. after &&
  116. after.includes('\n') &&
  117. after.slice(after.lastIndexOf('\n') + 1) !== expectedClosingBraceIndentation
  118. ) {
  119. if (context.fix) {
  120. node.raws.after = fixIndentation(node.raws.after, expectedClosingBraceIndentation);
  121. } else {
  122. report({
  123. message: messages.expected(legibleExpectation(closingBraceLevel)),
  124. node,
  125. index: node.toString().length - 1,
  126. result,
  127. ruleName,
  128. });
  129. }
  130. }
  131. // If this is a declaration, check the value
  132. if (isDeclaration(node)) {
  133. checkValue(node, nodeLevel);
  134. }
  135. // If this is a rule, check the selector
  136. if (isRule(node)) {
  137. checkSelector(node, nodeLevel);
  138. }
  139. // If this is an at rule, check the params
  140. if (isAtRule(node)) {
  141. checkAtRuleParams(node, nodeLevel);
  142. }
  143. });
  144. /**
  145. * @param {import('postcss').Node} node
  146. * @param {number} level
  147. * @returns {number}
  148. */
  149. function indentationLevel(node, level = 0) {
  150. if (!node.parent) throw new Error('A parent node must be present');
  151. if (isRoot(node.parent)) {
  152. return level + getRootBaseIndentLevel(node.parent, baseIndentLevel, primary);
  153. }
  154. let calculatedLevel;
  155. // Indentation level equals the ancestor nodes
  156. // separating this node from root; so recursively
  157. // run this operation
  158. calculatedLevel = indentationLevel(node.parent, level + 1);
  159. // If `secondaryOptions.except` includes "block",
  160. // blocks are taken down one from their calculated level
  161. // (all blocks are the same level as their parents)
  162. if (
  163. optionsMatches(secondaryOptions, 'except', 'block') &&
  164. (isRule(node) || isAtRule(node)) &&
  165. hasBlock(node)
  166. ) {
  167. calculatedLevel--;
  168. }
  169. return calculatedLevel;
  170. }
  171. /**
  172. * @param {import('postcss').Declaration} decl
  173. * @param {number} declLevel
  174. */
  175. function checkValue(decl, declLevel) {
  176. if (!decl.value.includes('\n')) {
  177. return;
  178. }
  179. if (optionsMatches(secondaryOptions, 'ignore', 'value')) {
  180. return;
  181. }
  182. const declString = decl.toString();
  183. const valueLevel = optionsMatches(secondaryOptions, 'except', 'value')
  184. ? declLevel
  185. : declLevel + 1;
  186. checkMultilineBit(declString, valueLevel, decl);
  187. }
  188. /**
  189. * @param {import('postcss').Rule} ruleNode
  190. * @param {number} ruleLevel
  191. */
  192. function checkSelector(ruleNode, ruleLevel) {
  193. const selector = ruleNode.selector;
  194. // Less mixins have params, and they should be indented extra
  195. // @ts-expect-error -- TS2339: Property 'params' does not exist on type 'Rule'.
  196. if (ruleNode.params) {
  197. ruleLevel += 1;
  198. }
  199. checkMultilineBit(selector, ruleLevel, ruleNode);
  200. }
  201. /**
  202. * @param {import('postcss').AtRule} atRule
  203. * @param {number} ruleLevel
  204. */
  205. function checkAtRuleParams(atRule, ruleLevel) {
  206. if (optionsMatches(secondaryOptions, 'ignore', 'param')) {
  207. return;
  208. }
  209. // @nest and SCSS's @at-root rules should be treated like regular rules, not expected
  210. // to have their params (selectors) indented
  211. const paramLevel =
  212. optionsMatches(secondaryOptions, 'except', 'param') ||
  213. atRule.name === 'nest' ||
  214. atRule.name === 'at-root'
  215. ? ruleLevel
  216. : ruleLevel + 1;
  217. checkMultilineBit(beforeBlockString(atRule).trim(), paramLevel, atRule);
  218. }
  219. /**
  220. * @param {string} source
  221. * @param {number} newlineIndentLevel
  222. * @param {import('postcss').Node} node
  223. */
  224. function checkMultilineBit(source, newlineIndentLevel, node) {
  225. if (!source.includes('\n')) {
  226. return;
  227. }
  228. // Data for current node fixing
  229. /** @type {Array<{ expectedIndentation: string, currentIndentation: string, startIndex: number }>} */
  230. const fixPositions = [];
  231. // `outsideParens` because function arguments and also non-standard parenthesized stuff like
  232. // Sass maps are ignored to allow for arbitrary indentation
  233. let parentheticalDepth = 0;
  234. const ignoreInsideParans = optionsMatches(secondaryOptions, 'ignore', 'inside-parens');
  235. styleSearch(
  236. {
  237. source,
  238. target: '\n',
  239. // @ts-expect-error -- The `outsideParens` option is unsupported. Why?
  240. outsideParens: ignoreInsideParans,
  241. },
  242. (match, matchCount) => {
  243. const precedesClosingParenthesis = /^[ \t]*\)/.test(source.slice(match.startIndex + 1));
  244. if (ignoreInsideParans && (precedesClosingParenthesis || match.insideParens)) {
  245. return;
  246. }
  247. let expectedIndentLevel = newlineIndentLevel;
  248. // Modififications for parenthetical content
  249. if (!ignoreInsideParans && match.insideParens) {
  250. // If the first match in is within parentheses, reduce the parenthesis penalty
  251. if (matchCount === 1) parentheticalDepth -= 1;
  252. // Account for windows line endings
  253. let newlineIndex = match.startIndex;
  254. if (source[match.startIndex - 1] === '\r') {
  255. newlineIndex--;
  256. }
  257. const followsOpeningParenthesis = /\([ \t]*$/.test(source.slice(0, newlineIndex));
  258. if (followsOpeningParenthesis) {
  259. parentheticalDepth += 1;
  260. }
  261. const followsOpeningBrace = /\{[ \t]*$/.test(source.slice(0, newlineIndex));
  262. if (followsOpeningBrace) {
  263. parentheticalDepth += 1;
  264. }
  265. const startingClosingBrace = /^[ \t]*\}/.test(source.slice(match.startIndex + 1));
  266. if (startingClosingBrace) {
  267. parentheticalDepth -= 1;
  268. }
  269. expectedIndentLevel += parentheticalDepth;
  270. // Past this point, adjustments to parentheticalDepth affect next line
  271. if (precedesClosingParenthesis) {
  272. parentheticalDepth -= 1;
  273. }
  274. switch (secondaryOptions.indentInsideParens) {
  275. case 'twice':
  276. if (!precedesClosingParenthesis || indentClosingBrace) {
  277. expectedIndentLevel += 1;
  278. }
  279. break;
  280. case 'once-at-root-twice-in-block':
  281. if (node.parent === node.root()) {
  282. if (precedesClosingParenthesis && !indentClosingBrace) {
  283. expectedIndentLevel -= 1;
  284. }
  285. break;
  286. }
  287. if (!precedesClosingParenthesis || indentClosingBrace) {
  288. expectedIndentLevel += 1;
  289. }
  290. break;
  291. default:
  292. if (precedesClosingParenthesis && !indentClosingBrace) {
  293. expectedIndentLevel -= 1;
  294. }
  295. }
  296. }
  297. // Starting at the index after the newline, we want to
  298. // check that the whitespace characters (excluding newlines) before the first
  299. // non-whitespace character equal the expected indentation
  300. const afterNewlineSpaceMatches = /^([ \t]*)\S/.exec(source.slice(match.startIndex + 1));
  301. if (!afterNewlineSpaceMatches) {
  302. return;
  303. }
  304. const afterNewlineSpace = afterNewlineSpaceMatches[1];
  305. const expectedIndentation = indentChar.repeat(
  306. expectedIndentLevel > 0 ? expectedIndentLevel : 0,
  307. );
  308. if (afterNewlineSpace !== expectedIndentation) {
  309. if (context.fix) {
  310. // Adding fixes position in reverse order, because if we change indent in the beginning of the string it will break all following fixes for that string
  311. fixPositions.unshift({
  312. expectedIndentation,
  313. currentIndentation: afterNewlineSpace,
  314. startIndex: match.startIndex,
  315. });
  316. } else {
  317. report({
  318. message: messages.expected(legibleExpectation(expectedIndentLevel)),
  319. node,
  320. index: match.startIndex + afterNewlineSpace.length + 1,
  321. result,
  322. ruleName,
  323. });
  324. }
  325. }
  326. },
  327. );
  328. if (fixPositions.length) {
  329. if (isRule(node)) {
  330. for (const fixPosition of fixPositions) {
  331. node.selector = replaceIndentation(
  332. node.selector,
  333. fixPosition.currentIndentation,
  334. fixPosition.expectedIndentation,
  335. fixPosition.startIndex,
  336. );
  337. }
  338. }
  339. if (isDeclaration(node)) {
  340. const declProp = node.prop;
  341. const declBetween = node.raws.between;
  342. if (!isString(declBetween)) {
  343. throw new TypeError('The `between` property must be a string');
  344. }
  345. for (const fixPosition of fixPositions) {
  346. if (fixPosition.startIndex < declProp.length + declBetween.length) {
  347. node.raws.between = replaceIndentation(
  348. declBetween,
  349. fixPosition.currentIndentation,
  350. fixPosition.expectedIndentation,
  351. fixPosition.startIndex - declProp.length,
  352. );
  353. } else {
  354. node.value = replaceIndentation(
  355. node.value,
  356. fixPosition.currentIndentation,
  357. fixPosition.expectedIndentation,
  358. fixPosition.startIndex - declProp.length - declBetween.length,
  359. );
  360. }
  361. }
  362. }
  363. if (isAtRule(node)) {
  364. const atRuleName = node.name;
  365. const atRuleAfterName = node.raws.afterName;
  366. const atRuleParams = node.params;
  367. if (!isString(atRuleAfterName)) {
  368. throw new TypeError('The `afterName` property must be a string');
  369. }
  370. for (const fixPosition of fixPositions) {
  371. // 1 — it's a @ length
  372. if (fixPosition.startIndex < 1 + atRuleName.length + atRuleAfterName.length) {
  373. node.raws.afterName = replaceIndentation(
  374. atRuleAfterName,
  375. fixPosition.currentIndentation,
  376. fixPosition.expectedIndentation,
  377. fixPosition.startIndex - atRuleName.length - 1,
  378. );
  379. } else {
  380. node.params = replaceIndentation(
  381. atRuleParams,
  382. fixPosition.currentIndentation,
  383. fixPosition.expectedIndentation,
  384. fixPosition.startIndex - atRuleName.length - atRuleAfterName.length - 1,
  385. );
  386. }
  387. }
  388. }
  389. }
  390. }
  391. };
  392. };
  393. /**
  394. * @param {import('postcss').Root} root
  395. * @param {number | 'auto'} baseIndentLevel
  396. * @param {string} space
  397. * @returns {number}
  398. */
  399. function getRootBaseIndentLevel(root, baseIndentLevel, space) {
  400. const document = getDocument(root);
  401. if (!document) {
  402. return 0;
  403. }
  404. if (!root.source) {
  405. throw new Error('The root node must have a source');
  406. }
  407. /** @type {import('postcss').Source & { baseIndentLevel?: number }} */
  408. const source = root.source;
  409. const indentLevel = source.baseIndentLevel;
  410. if (isNumber(indentLevel) && Number.isSafeInteger(indentLevel)) {
  411. return indentLevel;
  412. }
  413. const newIndentLevel = inferRootIndentLevel(root, baseIndentLevel, () =>
  414. inferDocIndentSize(document, space),
  415. );
  416. source.baseIndentLevel = newIndentLevel;
  417. return newIndentLevel;
  418. }
  419. /**
  420. * @param {import('postcss').Node} node
  421. */
  422. function getDocument(node) {
  423. // @ts-expect-error -- TS2339: Property 'document' does not exist on type 'Node'.
  424. const document = node.document;
  425. if (document) {
  426. return document;
  427. }
  428. const root = node.root();
  429. // @ts-expect-error -- TS2339: Property 'document' does not exist on type 'Node'.
  430. return root && root.document;
  431. }
  432. /**
  433. * @param {import('postcss').Document} document
  434. * @param {string} space
  435. * returns {number}
  436. */
  437. function inferDocIndentSize(document, space) {
  438. if (!document.source) throw new Error('The document node must have a source');
  439. /** @type {import('postcss').Source & { indentSize?: number }} */
  440. const docSource = document.source;
  441. let indentSize = docSource.indentSize;
  442. if (isNumber(indentSize) && Number.isSafeInteger(indentSize)) {
  443. return indentSize;
  444. }
  445. const source = document.source.input.css;
  446. const indents = source.match(/^ *(?=\S)/gm);
  447. if (indents) {
  448. /** @type {Map<number, number>} */
  449. const scores = new Map();
  450. let lastIndentSize = 0;
  451. let lastLeadingSpacesLength = 0;
  452. /**
  453. * @param {number} leadingSpacesLength
  454. */
  455. const vote = (leadingSpacesLength) => {
  456. if (leadingSpacesLength) {
  457. lastIndentSize = Math.abs(leadingSpacesLength - lastLeadingSpacesLength) || lastIndentSize;
  458. if (lastIndentSize > 1) {
  459. const score = scores.get(lastIndentSize);
  460. if (score) {
  461. scores.set(lastIndentSize, score + 1);
  462. } else {
  463. scores.set(lastIndentSize, 1);
  464. }
  465. }
  466. } else {
  467. lastIndentSize = 0;
  468. }
  469. lastLeadingSpacesLength = leadingSpacesLength;
  470. };
  471. for (const leadingSpaces of indents) {
  472. vote(leadingSpaces.length);
  473. }
  474. let bestScore = 0;
  475. for (const [indentSizeDate, score] of scores.entries()) {
  476. if (score > bestScore) {
  477. bestScore = score;
  478. indentSize = indentSizeDate;
  479. }
  480. }
  481. }
  482. indentSize = Number(indentSize) || (indents && indents[0].length) || Number(space) || 2;
  483. docSource.indentSize = indentSize;
  484. return indentSize;
  485. }
  486. /**
  487. * @param {import('postcss').Root} root
  488. * @param {number | 'auto'} baseIndentLevel
  489. * @param {() => number} indentSize
  490. * @returns {number}
  491. */
  492. function inferRootIndentLevel(root, baseIndentLevel, indentSize) {
  493. /**
  494. * @param {string} indent
  495. */
  496. function getIndentLevel(indent) {
  497. const tabMatch = indent.match(/\t/g);
  498. const tabCount = tabMatch ? tabMatch.length : 0;
  499. const spaceMatch = indent.match(/ /g);
  500. const spaceCount = spaceMatch ? Math.round(spaceMatch.length / indentSize()) : 0;
  501. return tabCount + spaceCount;
  502. }
  503. let newBaseIndentLevel = 0;
  504. if (!isNumber(baseIndentLevel) || !Number.isSafeInteger(baseIndentLevel)) {
  505. if (!root.source) throw new Error('The root node must have a source');
  506. let source = root.source.input.css;
  507. source = source.replace(/^[^\r\n]+/, (firstLine) => {
  508. const match = root.raws.codeBefore && /(?:^|\n)([ \t]*)$/.exec(root.raws.codeBefore);
  509. if (match) {
  510. return match[1] + firstLine;
  511. }
  512. return '';
  513. });
  514. const indents = source.match(/^[ \t]*(?=\S)/gm);
  515. if (indents) {
  516. return Math.min(...indents.map((indent) => getIndentLevel(indent)));
  517. }
  518. newBaseIndentLevel = 1;
  519. } else {
  520. newBaseIndentLevel = baseIndentLevel;
  521. }
  522. const indents = [];
  523. const foundIndents = root.raws.codeBefore && /(?:^|\n)([ \t]*)\S/m.exec(root.raws.codeBefore);
  524. // The indent level of the CSS code block in non-CSS-like files is determined by the shortest indent of non-empty line.
  525. if (foundIndents) {
  526. let shortest = Number.MAX_SAFE_INTEGER;
  527. let i = 0;
  528. while (++i < foundIndents.length) {
  529. const current = getIndentLevel(foundIndents[i]);
  530. if (current < shortest) {
  531. shortest = current;
  532. if (shortest === 0) {
  533. break;
  534. }
  535. }
  536. }
  537. if (shortest !== Number.MAX_SAFE_INTEGER) {
  538. indents.push(new Array(shortest).fill(' ').join(''));
  539. }
  540. }
  541. const after = root.raws.after;
  542. if (after) {
  543. let afterEnd;
  544. if (after.endsWith('\n')) {
  545. // @ts-expect-error -- TS2339: Property 'document' does not exist on type 'Root'.
  546. const document = root.document;
  547. if (document) {
  548. const nextRoot = document.nodes[document.nodes.indexOf(root) + 1];
  549. afterEnd = nextRoot ? nextRoot.raws.codeBefore : document.raws.codeAfter;
  550. } else {
  551. // Nested root node in css-in-js lang
  552. const parent = root.parent;
  553. if (!parent) throw new Error('The root node must have a parent');
  554. const nextRoot = parent.nodes[parent.nodes.indexOf(root) + 1];
  555. afterEnd = nextRoot ? nextRoot.raws.codeBefore : root.raws.codeAfter;
  556. }
  557. } else {
  558. afterEnd = after;
  559. }
  560. if (afterEnd) indents.push(afterEnd.match(/^[ \t]*/)[0]);
  561. }
  562. if (indents.length) {
  563. return Math.max(...indents.map((indent) => getIndentLevel(indent))) + newBaseIndentLevel;
  564. }
  565. return newBaseIndentLevel;
  566. }
  567. /**
  568. * @param {string | undefined} str
  569. * @param {string} whitespace
  570. */
  571. function fixIndentation(str, whitespace) {
  572. if (!isString(str)) {
  573. return str;
  574. }
  575. return str.replace(/\n[ \t]*(?=\S|$)/g, `\n${whitespace}`);
  576. }
  577. /**
  578. * @param {string} input
  579. * @param {string} searchString
  580. * @param {string} replaceString
  581. * @param {number} startIndex
  582. */
  583. function replaceIndentation(input, searchString, replaceString, startIndex) {
  584. const offset = startIndex + 1;
  585. const stringStart = input.slice(0, offset);
  586. const stringEnd = input.slice(offset + searchString.length);
  587. return stringStart + replaceString + stringEnd;
  588. }
  589. rule.ruleName = ruleName;
  590. rule.messages = messages;
  591. rule.meta = meta;
  592. module.exports = rule;