index.js 20 KB

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