index.js 20 KB

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