f339fa6c8f34c9e2704fa3682773dedd45470381
[dotfiles/.git] / no-irregular-whitespace.js
1 /**
2  * @fileoverview Rule to disalow whitespace that is not a tab or space, whitespace inside strings and comments are allowed
3  * @author Jonathan Kingston
4  * @author Christophe Porteneuve
5  */
6
7 "use strict";
8
9 //------------------------------------------------------------------------------
10 // Requirements
11 //------------------------------------------------------------------------------
12
13 const astUtils = require("./utils/ast-utils");
14
15 //------------------------------------------------------------------------------
16 // Constants
17 //------------------------------------------------------------------------------
18
19 const ALL_IRREGULARS = /[\f\v\u0085\ufeff\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000\u2028\u2029]/u;
20 const IRREGULAR_WHITESPACE = /[\f\v\u0085\ufeff\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u202f\u205f\u3000]+/mgu;
21 const IRREGULAR_LINE_TERMINATORS = /[\u2028\u2029]/mgu;
22 const LINE_BREAK = astUtils.createGlobalLinebreakMatcher();
23
24 //------------------------------------------------------------------------------
25 // Rule Definition
26 //------------------------------------------------------------------------------
27
28 module.exports = {
29     meta: {
30         type: "problem",
31
32         docs: {
33             description: "disallow irregular whitespace",
34             category: "Possible Errors",
35             recommended: true,
36             url: "https://eslint.org/docs/rules/no-irregular-whitespace"
37         },
38
39         schema: [
40             {
41                 type: "object",
42                 properties: {
43                     skipComments: {
44                         type: "boolean",
45                         default: false
46                     },
47                     skipStrings: {
48                         type: "boolean",
49                         default: true
50                     },
51                     skipTemplates: {
52                         type: "boolean",
53                         default: false
54                     },
55                     skipRegExps: {
56                         type: "boolean",
57                         default: false
58                     }
59                 },
60                 additionalProperties: false
61             }
62         ]
63     },
64
65     create(context) {
66
67         // Module store of errors that we have found
68         let errors = [];
69
70         // Lookup the `skipComments` option, which defaults to `false`.
71         const options = context.options[0] || {};
72         const skipComments = !!options.skipComments;
73         const skipStrings = options.skipStrings !== false;
74         const skipRegExps = !!options.skipRegExps;
75         const skipTemplates = !!options.skipTemplates;
76
77         const sourceCode = context.getSourceCode();
78         const commentNodes = sourceCode.getAllComments();
79
80         /**
81          * Removes errors that occur inside a string node
82          * @param {ASTNode} node to check for matching errors.
83          * @returns {void}
84          * @private
85          */
86         function removeWhitespaceError(node) {
87             const locStart = node.loc.start;
88             const locEnd = node.loc.end;
89
90             errors = errors.filter(({ loc: errorLoc }) => {
91                 if (errorLoc.line >= locStart.line && errorLoc.line <= locEnd.line) {
92                     if (errorLoc.column >= locStart.column && (errorLoc.column <= locEnd.column || errorLoc.line < locEnd.line)) {
93                         return false;
94                     }
95                 }
96                 return true;
97             });
98         }
99
100         /**
101          * Checks identifier or literal nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors
102          * @param {ASTNode} node to check for matching errors.
103          * @returns {void}
104          * @private
105          */
106         function removeInvalidNodeErrorsInIdentifierOrLiteral(node) {
107             const shouldCheckStrings = skipStrings && (typeof node.value === "string");
108             const shouldCheckRegExps = skipRegExps && Boolean(node.regex);
109
110             if (shouldCheckStrings || shouldCheckRegExps) {
111
112                 // If we have irregular characters remove them from the errors list
113                 if (ALL_IRREGULARS.test(node.raw)) {
114                     removeWhitespaceError(node);
115                 }
116             }
117         }
118
119         /**
120          * Checks template string literal nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors
121          * @param {ASTNode} node to check for matching errors.
122          * @returns {void}
123          * @private
124          */
125         function removeInvalidNodeErrorsInTemplateLiteral(node) {
126             if (typeof node.value.raw === "string") {
127                 if (ALL_IRREGULARS.test(node.value.raw)) {
128                     removeWhitespaceError(node);
129                 }
130             }
131         }
132
133         /**
134          * Checks comment nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors
135          * @param {ASTNode} node to check for matching errors.
136          * @returns {void}
137          * @private
138          */
139         function removeInvalidNodeErrorsInComment(node) {
140             if (ALL_IRREGULARS.test(node.value)) {
141                 removeWhitespaceError(node);
142             }
143         }
144
145         /**
146          * Checks the program source for irregular whitespace
147          * @param {ASTNode} node The program node
148          * @returns {void}
149          * @private
150          */
151         function checkForIrregularWhitespace(node) {
152             const sourceLines = sourceCode.lines;
153
154             sourceLines.forEach((sourceLine, lineIndex) => {
155                 const lineNumber = lineIndex + 1;
156                 let match;
157
158                 while ((match = IRREGULAR_WHITESPACE.exec(sourceLine)) !== null) {
159                     const location = {
160                         line: lineNumber,
161                         column: match.index
162                     };
163
164                     errors.push({ node, message: "Irregular whitespace not allowed.", loc: location });
165                 }
166             });
167         }
168
169         /**
170          * Checks the program source for irregular line terminators
171          * @param {ASTNode} node The program node
172          * @returns {void}
173          * @private
174          */
175         function checkForIrregularLineTerminators(node) {
176             const source = sourceCode.getText(),
177                 sourceLines = sourceCode.lines,
178                 linebreaks = source.match(LINE_BREAK);
179             let lastLineIndex = -1,
180                 match;
181
182             while ((match = IRREGULAR_LINE_TERMINATORS.exec(source)) !== null) {
183                 const lineIndex = linebreaks.indexOf(match[0], lastLineIndex + 1) || 0;
184                 const location = {
185                     line: lineIndex + 1,
186                     column: sourceLines[lineIndex].length
187                 };
188
189                 errors.push({ node, message: "Irregular whitespace not allowed.", loc: location });
190                 lastLineIndex = lineIndex;
191             }
192         }
193
194         /**
195          * A no-op function to act as placeholder for comment accumulation when the `skipComments` option is `false`.
196          * @returns {void}
197          * @private
198          */
199         function noop() {}
200
201         const nodes = {};
202
203         if (ALL_IRREGULARS.test(sourceCode.getText())) {
204             nodes.Program = function(node) {
205
206                 /*
207                  * As we can easily fire warnings for all white space issues with
208                  * all the source its simpler to fire them here.
209                  * This means we can check all the application code without having
210                  * to worry about issues caused in the parser tokens.
211                  * When writing this code also evaluating per node was missing out
212                  * connecting tokens in some cases.
213                  * We can later filter the errors when they are found to be not an
214                  * issue in nodes we don't care about.
215                  */
216                 checkForIrregularWhitespace(node);
217                 checkForIrregularLineTerminators(node);
218             };
219
220             nodes.Identifier = removeInvalidNodeErrorsInIdentifierOrLiteral;
221             nodes.Literal = removeInvalidNodeErrorsInIdentifierOrLiteral;
222             nodes.TemplateElement = skipTemplates ? removeInvalidNodeErrorsInTemplateLiteral : noop;
223             nodes["Program:exit"] = function() {
224                 if (skipComments) {
225
226                     // First strip errors occurring in comment nodes.
227                     commentNodes.forEach(removeInvalidNodeErrorsInComment);
228                 }
229
230                 // If we have any errors remaining report on them
231                 errors.forEach(error => context.report(error));
232             };
233         } else {
234             nodes.Program = noop;
235         }
236
237         return nodes;
238     }
239 };