Skip to content

Commit

Permalink
Add no-irregular-whitespace (#5209)
Browse files Browse the repository at this point in the history
  • Loading branch information
itutto committed Apr 20, 2021
1 parent 916e3bb commit 61fe25d
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/user-guide/rules/list.md
Expand Up @@ -395,3 +395,4 @@ Grouped first by the following categories and then by the [_thing_](http://apps.
- [`no-missing-end-of-source-newline`](../../../lib/rules/no-missing-end-of-source-newline/README.md): Disallow missing end-of-source newlines (Autofixable).
- [`no-empty-first-line`](../../../lib/rules/no-empty-first-line/README.md): Disallow empty first lines (Autofixable).
- [`unicode-bom`](../../../lib/rules/unicode-bom/README.md): Require or disallow Unicode BOM.
- [`no-irregular-whitespace`](../../../lib/rules/no-irregular-whitespace/README.md): Disallow irregular whitespace.
1 change: 1 addition & 0 deletions lib/rules/index.js
Expand Up @@ -245,6 +245,7 @@ const rules = {
'no-invalid-position-at-import-rule': importLazy(() =>
require('./no-invalid-position-at-import-rule'),
)(),
'no-irregular-whitespace': importLazy(() => require('./no-irregular-whitespace'))(),
'no-missing-end-of-source-newline': importLazy(() =>
require('./no-missing-end-of-source-newline'),
)(),
Expand Down
57 changes: 57 additions & 0 deletions lib/rules/no-irregular-whitespace/README.md
@@ -0,0 +1,57 @@
# no-irregular-whitespace

Disallow irregular whitespaces.

<!-- prettier-ignore -->
```css
.firstClass .secondClass {}
/** ↑
* Irregular whitespace. Selector would fail to match '.firstClass' */
```

## Options

### `true`

The following patterns are considered violations:

<!-- prettier-ignore -->
```css
.firstClass .secondClass {}
```

The following patterns are _not_ considered violations:

<!-- prettier-ignore -->
```css
.firstClass .secondClass { /* Writing comments with irregular whitespaces */ }
```

## Unicode reference of irregular whitespaces

```
\u000B - Line Tabulation (\v) - <VT>
\u000C - Form Feed (\f) - <FF>
\u00A0 - No-Break Space - <NBSP>
\u0085 - Next Line
\u1680 - Ogham Space Mark
\u180E - Mongolian Vowel Separator - <MVS>
\uFEFF - Zero Width No-Break Space - <BOM>
\u2000 - En Quad
\u2001 - Em Quad
\u2002 - En Space - <ENSP>
\u2003 - Em Space - <EMSP>
\u2004 - Tree-Per-Em
\u2005 - Four-Per-Em
\u2006 - Six-Per-Em
\u2007 - Figure Space
\u2008 - Punctuation Space - <PUNCSP>
\u2009 - Thin Space
\u200A - Hair Space
\u200B - Zero Width Space - <ZWSP>
\u2028 - Line Separator
\u2029 - Paragraph Separator
\u202F - Narrow No-Break Space
\u205F - Medium Mathematical Space
\u3000 - Ideographic Space
```
98 changes: 98 additions & 0 deletions lib/rules/no-irregular-whitespace/__tests__/index.js
@@ -0,0 +1,98 @@
'use strict';

const { messages, ruleName } = require('..');

const IRREGULAR_WHITESPACES = [
'\u000B', // Line Tabulation (\v) - <VT>
'\u000C', // Form Feed (\f) - <FF>
'\u00A0', // No-Break Space - <NBSP>
'\u0085', // Next Line
'\u1680', // Ogham Space Mark
'\u180E', // Mongolian Vowel Separator - <MVS>
'\uFEFF', // Zero Width No-Break Space - <BOM>
'\u2000', // En Quad
'\u2001', // Em Quad
'\u2002', // En Space - <ENSP>
'\u2003', // Em Space - <EMSP>
'\u2004', // Tree-Per-Em
'\u2005', // Four-Per-Em
'\u2006', // Six-Per-Em
'\u2007', // Figure Space
'\u2008', // Punctuation Space - <PUNCSP>
'\u2009', // Thin Space
'\u200A', // Hair Space
'\u200B', // Zero Width Space - <ZWSP>
'\u2028', // Line Separator
'\u2029', // Paragraph Separator
'\u202F', // Narrow No-Break Space
'\u205F', // Medium Mathematical Space
'\u3000', // Ideographic Space
];

const characterToUnicodeString = (str) =>
`\\u${str.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')}`;

testRule({
ruleName,
config: true,
accept: [
{
code: ' ',
description: 'regular whitespace',
},
{
code: '\n',
description: 'new line',
},
{
code: '/* Irregular whitespace\nin multi line comments\nare allowed */',
description: 'irregular whitespace in multi line comments',
},
],

reject: [
{
code: '.firstClass .secondClass { color: pink; }',
description: 'irregular whitespace in selector',
message: messages.unexpected,
line: 1,
column: 12,
},
{
code: '.firstClass .secondClass  { color: pink; }',
description: 'irregular whitespace after selector',
message: messages.unexpected,
line: 1,
column: 26,
},
{
code: 'margin: 1rem 2rem;',
description: 'irregular whitespace in declaration value',
message: messages.unexpected,
line: 1,
column: 13,
},
{
code: 'margin: 1rem 2rem ;',
description: 'irregular whitespace after value',
message: messages.unexpected,
line: 1,
column: 18,
},
{
code: '$variable : 5rem;',
description: 'irregular whitespace in variable name',
message: messages.unexpected,
line: 1,
column: 10,
},
// Generic test for all types of irregular whitespaces.
...IRREGULAR_WHITESPACES.map((ws) => ({
code: `a[title="irregular${ws}whitespace"] { color: pink; }`,
description: `irregular whitespace in attribute selector: ${characterToUnicodeString(ws)}`,
message: messages.unexpected,
line: 1,
column: 19,
})),
],
});
134 changes: 134 additions & 0 deletions lib/rules/no-irregular-whitespace/index.js
@@ -0,0 +1,134 @@
// @ts-nocheck

'use strict';

const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');

const ruleName = 'no-irregular-whitespace';
const messages = ruleMessages(ruleName, {
unexpected: 'Unexpected irregular whitespace',
});

const IRREGULAR_WHITESPACES = [
'\u000B', // Line Tabulation (\v) - <VT>
'\u000C', // Form Feed (\f) - <FF>
'\u00A0', // No-Break Space - <NBSP>
'\u0085', // Next Line
'\u1680', // Ogham Space Mark
'\u180E', // Mongolian Vowel Separator - <MVS>
'\uFEFF', // Zero Width No-Break Space - <BOM>
'\u2000', // En Quad
'\u2001', // Em Quad
'\u2002', // En Space - <ENSP>
'\u2003', // Em Space - <EMSP>
'\u2004', // Tree-Per-Em
'\u2005', // Four-Per-Em
'\u2006', // Six-Per-Em
'\u2007', // Figure Space
'\u2008', // Punctuation Space - <PUNCSP>
'\u2009', // Thin Space
'\u200A', // Hair Space
'\u200B', // Zero Width Space - <ZWSP>
'\u2028', // Line Separator
'\u2029', // Paragraph Separator
'\u202F', // Narrow No-Break Space
'\u205F', // Medium Mathematical Space
'\u3000', // Ideographic Space
];

const IRREGULAR_WHITESPACES_PATTERN = new RegExp(`([${IRREGULAR_WHITESPACES.join('')}])`);

const generateInvalidWhitespaceValidator = () => {
return (str) => typeof str === 'string' && IRREGULAR_WHITESPACES_PATTERN.exec(str);
};

const declarationSchema = {
prop: 'string',
value: 'string',
raws: {
before: 'string',
between: 'string',
},
};

const atRuleSchema = {
name: 'string',
params: 'string',
raws: {
before: 'string',
between: 'string',
afterName: 'string',
after: 'string',
},
};

const ruleSchema = {
selector: 'string',
raws: {
before: 'string',
between: 'string',
after: 'string',
},
};

const generateNodeValidator = (nodeSchema, validator) => {
const allKeys = Object.keys(nodeSchema);
const validatorForKey = {};

allKeys.forEach((key) => {
if (typeof nodeSchema[key] === 'string') validatorForKey[key] = validator;

if (typeof nodeSchema[key] === 'object')
validatorForKey[key] = generateNodeValidator(nodeSchema[key], validator);
});

// This will be called many times, so it's optimized for performance and not readibility.
// Surprisingly, this seem to be slightly faster then concatenating the params and running the validator once.
return (node) => {
for (const currentKey of allKeys) {
if (validatorForKey[currentKey](node[currentKey])) {
return validatorForKey[currentKey](node[currentKey]);
}
}
};
};

function rule(on) {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual: on });

if (!validOptions) {
return;
}

const genericValidator = generateInvalidWhitespaceValidator();

const validate = (node, validator) => {
const issue = validator(node);

if (issue) {
report({
ruleName,
result,
message: messages.unexpected,
node,
word: issue[1],
});
}
};

const atRuleValidator = generateNodeValidator(atRuleSchema, genericValidator);
const ruleValidator = generateNodeValidator(ruleSchema, genericValidator);
const declValidator = generateNodeValidator(declarationSchema, genericValidator);

root.walkAtRules((atRule) => validate(atRule, atRuleValidator));
root.walkRules((selector) => validate(selector, ruleValidator));
root.walkDecls((declaration) => validate(declaration, declValidator));
};
}

rule.ruleName = ruleName;
rule.messages = messages;
module.exports = rule;

0 comments on commit 61fe25d

Please sign in to comment.