whitespaceChecker.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. 'use strict';
  2. const configurationError = require('./configurationError');
  3. const isSingleLineString = require('./isSingleLineString');
  4. const isWhitespace = require('./isWhitespace');
  5. /**
  6. * @typedef {(message: string) => string} MessageFunction
  7. */
  8. /**
  9. * @typedef {Object} Messages
  10. * @property {MessageFunction} [expectedBefore]
  11. * @property {MessageFunction} [rejectedBefore]
  12. * @property {MessageFunction} [expectedAfter]
  13. * @property {MessageFunction} [rejectedAfter]
  14. * @property {MessageFunction} [expectedBeforeSingleLine]
  15. * @property {MessageFunction} [rejectedBeforeSingleLine]
  16. * @property {MessageFunction} [expectedBeforeMultiLine]
  17. * @property {MessageFunction} [rejectedBeforeMultiLine]
  18. * @property {MessageFunction} [expectedAfterSingleLine]
  19. * @property {MessageFunction} [rejectedAfterSingleLine]
  20. * @property {MessageFunction} [expectedAfterMultiLine]
  21. * @property {MessageFunction} [rejectedAfterMultiLine]
  22. */
  23. /**
  24. * @typedef {Object} WhitespaceCheckerArgs
  25. * @property {string} source - The source string
  26. * @property {number} index - The index of the character to check before
  27. * @property {(message: string) => void} err - If a problem is found, this callback
  28. * will be invoked with the relevant warning message.
  29. * Typically this callback will report() the problem.
  30. * @property {string} [errTarget] - If a problem is found, this string
  31. * will be sent to the relevant warning message.
  32. * @property {string} [lineCheckStr] - Single- and multi-line checkers
  33. * will use this string to determine whether they should proceed,
  34. * i.e. if this string is one line only, single-line checkers will check,
  35. * multi-line checkers will ignore.
  36. * If none is passed, they will use `source`.
  37. * @property {boolean} [onlyOneChar=false] - Only check *one* character before.
  38. * By default, "always-*" checks will look for the `targetWhitespace` one
  39. * before and then ensure there is no whitespace two before. This option
  40. * bypasses that second check.
  41. * @property {boolean} [allowIndentation=false] - Allow arbitrary indentation
  42. * between the `targetWhitespace` (almost definitely a newline) and the `index`.
  43. * With this option, the checker will see if a newline *begins* the whitespace before
  44. * the `index`.
  45. */
  46. /**
  47. * @typedef {(args: WhitespaceCheckerArgs) => void} WhitespaceChecker
  48. */
  49. /**
  50. * @typedef {{
  51. * before: WhitespaceChecker,
  52. * beforeAllowingIndentation: WhitespaceChecker,
  53. * after: WhitespaceChecker,
  54. * afterOneOnly: WhitespaceChecker,
  55. * }} WhitespaceCheckers
  56. */
  57. /**
  58. * Create a whitespaceChecker, which exposes the following functions:
  59. * - `before()`
  60. * - `beforeAllowingIndentation()`
  61. * - `after()`
  62. * - `afterOneOnly()`
  63. *
  64. * @param {"space" | "newline"} targetWhitespace - This is a keyword instead
  65. * of the actual character (e.g. " ") in order to accommodate
  66. * different styles of newline ("\n" vs "\r\n")
  67. * @param {"always" | "never" | "always-single-line" | "always-multi-line" | "never-single-line" | "never-multi-line"} expectation
  68. * @param {Messages} messages - An object of message functions;
  69. * calling `before*()` or `after*()` and the `expectation` that is passed
  70. * determines which message functions are required
  71. *
  72. * @returns {WhitespaceCheckers} The checker, with its exposed checking functions
  73. */
  74. module.exports = function whitespaceChecker(targetWhitespace, expectation, messages) {
  75. // Keep track of active arguments in order to avoid passing
  76. // too much stuff around, making signatures long and confusing.
  77. // This variable gets reset anytime a checking function is called.
  78. /** @type {WhitespaceCheckerArgs} */
  79. let activeArgs;
  80. /**
  81. * Check for whitespace *before* a character.
  82. * @type {WhitespaceChecker}
  83. */
  84. function before({
  85. source,
  86. index,
  87. err,
  88. errTarget,
  89. lineCheckStr,
  90. onlyOneChar = false,
  91. allowIndentation = false,
  92. }) {
  93. activeArgs = {
  94. source,
  95. index,
  96. err,
  97. errTarget,
  98. onlyOneChar,
  99. allowIndentation,
  100. };
  101. switch (expectation) {
  102. case 'always':
  103. expectBefore();
  104. break;
  105. case 'never':
  106. rejectBefore();
  107. break;
  108. case 'always-single-line':
  109. if (!isSingleLineString(lineCheckStr || source)) {
  110. return;
  111. }
  112. expectBefore(messages.expectedBeforeSingleLine);
  113. break;
  114. case 'never-single-line':
  115. if (!isSingleLineString(lineCheckStr || source)) {
  116. return;
  117. }
  118. rejectBefore(messages.rejectedBeforeSingleLine);
  119. break;
  120. case 'always-multi-line':
  121. if (isSingleLineString(lineCheckStr || source)) {
  122. return;
  123. }
  124. expectBefore(messages.expectedBeforeMultiLine);
  125. break;
  126. case 'never-multi-line':
  127. if (isSingleLineString(lineCheckStr || source)) {
  128. return;
  129. }
  130. rejectBefore(messages.rejectedBeforeMultiLine);
  131. break;
  132. default:
  133. throw configurationError(`Unknown expectation "${expectation}"`);
  134. }
  135. }
  136. /**
  137. * Check for whitespace *after* a character.
  138. * @type {WhitespaceChecker}
  139. */
  140. function after({ source, index, err, errTarget, lineCheckStr, onlyOneChar = false }) {
  141. activeArgs = { source, index, err, errTarget, onlyOneChar };
  142. switch (expectation) {
  143. case 'always':
  144. expectAfter();
  145. break;
  146. case 'never':
  147. rejectAfter();
  148. break;
  149. case 'always-single-line':
  150. if (!isSingleLineString(lineCheckStr || source)) {
  151. return;
  152. }
  153. expectAfter(messages.expectedAfterSingleLine);
  154. break;
  155. case 'never-single-line':
  156. if (!isSingleLineString(lineCheckStr || source)) {
  157. return;
  158. }
  159. rejectAfter(messages.rejectedAfterSingleLine);
  160. break;
  161. case 'always-multi-line':
  162. if (isSingleLineString(lineCheckStr || source)) {
  163. return;
  164. }
  165. expectAfter(messages.expectedAfterMultiLine);
  166. break;
  167. case 'never-multi-line':
  168. if (isSingleLineString(lineCheckStr || source)) {
  169. return;
  170. }
  171. rejectAfter(messages.rejectedAfterMultiLine);
  172. break;
  173. default:
  174. throw configurationError(`Unknown expectation "${expectation}"`);
  175. }
  176. }
  177. /**
  178. * @type {WhitespaceChecker}
  179. */
  180. function beforeAllowingIndentation(obj) {
  181. before({ ...obj, allowIndentation: true });
  182. }
  183. function expectBefore(messageFunc = messages.expectedBefore) {
  184. if (activeArgs.allowIndentation) {
  185. expectBeforeAllowingIndentation(messageFunc);
  186. return;
  187. }
  188. const _activeArgs = activeArgs;
  189. const source = _activeArgs.source;
  190. const index = _activeArgs.index;
  191. const oneCharBefore = source[index - 1];
  192. const twoCharsBefore = source[index - 2];
  193. if (!isValue(oneCharBefore)) {
  194. return;
  195. }
  196. if (
  197. targetWhitespace === 'space' &&
  198. oneCharBefore === ' ' &&
  199. (activeArgs.onlyOneChar || !isWhitespace(twoCharsBefore))
  200. ) {
  201. return;
  202. }
  203. assertFunction(messageFunc);
  204. activeArgs.err(messageFunc(activeArgs.errTarget || source[index]));
  205. }
  206. function expectBeforeAllowingIndentation(messageFunc = messages.expectedBefore) {
  207. const _activeArgs2 = activeArgs;
  208. const source = _activeArgs2.source;
  209. const index = _activeArgs2.index;
  210. const err = _activeArgs2.err;
  211. const expectedChar = (function () {
  212. if (targetWhitespace === 'newline') {
  213. return '\n';
  214. }
  215. })();
  216. let i = index - 1;
  217. while (source[i] !== expectedChar) {
  218. if (source[i] === '\t' || source[i] === ' ') {
  219. i--;
  220. continue;
  221. }
  222. assertFunction(messageFunc);
  223. err(messageFunc(activeArgs.errTarget || source[index]));
  224. return;
  225. }
  226. }
  227. function rejectBefore(messageFunc = messages.rejectedBefore) {
  228. const _activeArgs3 = activeArgs;
  229. const source = _activeArgs3.source;
  230. const index = _activeArgs3.index;
  231. const oneCharBefore = source[index - 1];
  232. if (isValue(oneCharBefore) && isWhitespace(oneCharBefore)) {
  233. assertFunction(messageFunc);
  234. activeArgs.err(messageFunc(activeArgs.errTarget || source[index]));
  235. }
  236. }
  237. /**
  238. * @type {WhitespaceChecker}
  239. */
  240. function afterOneOnly(obj) {
  241. after({ ...obj, onlyOneChar: true });
  242. }
  243. function expectAfter(messageFunc = messages.expectedAfter) {
  244. const _activeArgs4 = activeArgs;
  245. const source = _activeArgs4.source;
  246. const index = _activeArgs4.index;
  247. const oneCharAfter = source[index + 1];
  248. const twoCharsAfter = source[index + 2];
  249. if (!isValue(oneCharAfter)) {
  250. return;
  251. }
  252. if (targetWhitespace === 'newline') {
  253. // If index is followed by a Windows CR-LF ...
  254. if (
  255. oneCharAfter === '\r' &&
  256. twoCharsAfter === '\n' &&
  257. (activeArgs.onlyOneChar || !isWhitespace(source[index + 3]))
  258. ) {
  259. return;
  260. }
  261. // If index is followed by a Unix LF ...
  262. if (oneCharAfter === '\n' && (activeArgs.onlyOneChar || !isWhitespace(twoCharsAfter))) {
  263. return;
  264. }
  265. }
  266. if (
  267. targetWhitespace === 'space' &&
  268. oneCharAfter === ' ' &&
  269. (activeArgs.onlyOneChar || !isWhitespace(twoCharsAfter))
  270. ) {
  271. return;
  272. }
  273. assertFunction(messageFunc);
  274. activeArgs.err(messageFunc(activeArgs.errTarget || source[index]));
  275. }
  276. function rejectAfter(messageFunc = messages.rejectedAfter) {
  277. const _activeArgs5 = activeArgs;
  278. const source = _activeArgs5.source;
  279. const index = _activeArgs5.index;
  280. const oneCharAfter = source[index + 1];
  281. if (isValue(oneCharAfter) && isWhitespace(oneCharAfter)) {
  282. assertFunction(messageFunc);
  283. activeArgs.err(messageFunc(activeArgs.errTarget || source[index]));
  284. }
  285. }
  286. return {
  287. before,
  288. beforeAllowingIndentation,
  289. after,
  290. afterOneOnly,
  291. };
  292. };
  293. /**
  294. * @param {unknown} x
  295. */
  296. function isValue(x) {
  297. return x !== undefined && x !== null;
  298. }
  299. /**
  300. * @param {unknown} x
  301. * @returns {asserts x is Function}
  302. */
  303. function assertFunction(x) {
  304. if (typeof x !== 'function') {
  305. throw new TypeError(`\`${x}\` must be a function`);
  306. }
  307. }