Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add no-irregular-whitespace #5209

Merged
merged 7 commits into from Apr 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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('..');
itutto marked this conversation as resolved.
Show resolved Hide resolved

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;