324 lines
10 KiB
Plaintext
324 lines
10 KiB
Plaintext
/**
|
|
* @fileoverview Validates whitespace in and around the JSX opening and closing brackets
|
|
* @author Diogo Franco (Kovensky)
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const getTokenBeforeClosingBracket = require('../util/getTokenBeforeClosingBracket');
|
|
const docsUrl = require('../util/docsUrl');
|
|
const report = require('../util/report');
|
|
|
|
const messages = {
|
|
selfCloseSlashNoSpace: 'Whitespace is forbidden between `/` and `>`; write `/>`',
|
|
selfCloseSlashNeedSpace: 'Whitespace is required between `/` and `>`; write `/ >`',
|
|
closeSlashNoSpace: 'Whitespace is forbidden between `<` and `/`; write `</`',
|
|
closeSlashNeedSpace: 'Whitespace is required between `<` and `/`; write `< /`',
|
|
beforeSelfCloseNoSpace: 'A space is forbidden before closing bracket',
|
|
beforeSelfCloseNeedSpace: 'A space is required before closing bracket',
|
|
beforeSelfCloseNeedNewline: 'A newline is required before closing bracket',
|
|
afterOpenNoSpace: 'A space is forbidden after opening bracket',
|
|
afterOpenNeedSpace: 'A space is required after opening bracket',
|
|
beforeCloseNoSpace: 'A space is forbidden before closing bracket',
|
|
beforeCloseNeedSpace: 'Whitespace is required before closing bracket',
|
|
beforeCloseNeedNewline: 'A newline is required before closing bracket',
|
|
};
|
|
|
|
// ------------------------------------------------------------------------------
|
|
// Validators
|
|
// ------------------------------------------------------------------------------
|
|
|
|
function validateClosingSlash(context, node, option) {
|
|
const sourceCode = context.getSourceCode();
|
|
|
|
let adjacent;
|
|
|
|
if (node.selfClosing) {
|
|
const lastTokens = sourceCode.getLastTokens(node, 2);
|
|
|
|
adjacent = !sourceCode.isSpaceBetweenTokens(lastTokens[0], lastTokens[1]);
|
|
|
|
if (option === 'never') {
|
|
if (!adjacent) {
|
|
report(context, messages.selfCloseSlashNoSpace, 'selfCloseSlashNoSpace', {
|
|
node,
|
|
loc: {
|
|
start: lastTokens[0].loc.start,
|
|
end: lastTokens[1].loc.end,
|
|
},
|
|
fix(fixer) {
|
|
return fixer.removeRange([lastTokens[0].range[1], lastTokens[1].range[0]]);
|
|
},
|
|
});
|
|
}
|
|
} else if (option === 'always' && adjacent) {
|
|
report(context, messages.selfCloseSlashNeedSpace, 'selfCloseSlashNeedSpace', {
|
|
node,
|
|
loc: {
|
|
start: lastTokens[0].loc.start,
|
|
end: lastTokens[1].loc.end,
|
|
},
|
|
fix(fixer) {
|
|
return fixer.insertTextBefore(lastTokens[1], ' ');
|
|
},
|
|
});
|
|
}
|
|
} else {
|
|
const firstTokens = sourceCode.getFirstTokens(node, 2);
|
|
|
|
adjacent = !sourceCode.isSpaceBetweenTokens(firstTokens[0], firstTokens[1]);
|
|
|
|
if (option === 'never') {
|
|
if (!adjacent) {
|
|
report(context, messages.closeSlashNoSpace, 'closeSlashNoSpace', {
|
|
node,
|
|
loc: {
|
|
start: firstTokens[0].loc.start,
|
|
end: firstTokens[1].loc.end,
|
|
},
|
|
fix(fixer) {
|
|
return fixer.removeRange([firstTokens[0].range[1], firstTokens[1].range[0]]);
|
|
},
|
|
});
|
|
}
|
|
} else if (option === 'always' && adjacent) {
|
|
report(context, messages.closeSlashNeedSpace, 'closeSlashNeedSpace', {
|
|
node,
|
|
loc: {
|
|
start: firstTokens[0].loc.start,
|
|
end: firstTokens[1].loc.end,
|
|
},
|
|
fix(fixer) {
|
|
return fixer.insertTextBefore(firstTokens[1], ' ');
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function validateBeforeSelfClosing(context, node, option) {
|
|
const sourceCode = context.getSourceCode();
|
|
const leftToken = getTokenBeforeClosingBracket(node);
|
|
const closingSlash = sourceCode.getTokenAfter(leftToken);
|
|
|
|
if (node.loc.start.line !== node.loc.end.line && option === 'proportional-always') {
|
|
if (leftToken.loc.end.line === closingSlash.loc.start.line) {
|
|
report(context, messages.beforeSelfCloseNeedNewline, 'beforeSelfCloseNeedNewline', {
|
|
node,
|
|
loc: leftToken.loc.end,
|
|
fix(fixer) {
|
|
return fixer.insertTextBefore(closingSlash, '\n');
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (leftToken.loc.end.line !== closingSlash.loc.start.line) {
|
|
return;
|
|
}
|
|
|
|
const adjacent = !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash);
|
|
|
|
if ((option === 'always' || option === 'proportional-always') && adjacent) {
|
|
report(context, messages.beforeSelfCloseNeedSpace, 'beforeSelfCloseNeedSpace', {
|
|
node,
|
|
loc: closingSlash.loc.start,
|
|
fix(fixer) {
|
|
return fixer.insertTextBefore(closingSlash, ' ');
|
|
},
|
|
});
|
|
} else if (option === 'never' && !adjacent) {
|
|
report(context, messages.beforeSelfCloseNoSpace, 'beforeSelfCloseNoSpace', {
|
|
node,
|
|
loc: closingSlash.loc.start,
|
|
fix(fixer) {
|
|
const previousToken = sourceCode.getTokenBefore(closingSlash);
|
|
return fixer.removeRange([previousToken.range[1], closingSlash.range[0]]);
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
function validateAfterOpening(context, node, option) {
|
|
const sourceCode = context.getSourceCode();
|
|
const openingToken = sourceCode.getTokenBefore(node.name);
|
|
|
|
if (option === 'allow-multiline') {
|
|
if (openingToken.loc.start.line !== node.name.loc.start.line) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const adjacent = !sourceCode.isSpaceBetweenTokens(openingToken, node.name);
|
|
|
|
if (option === 'never' || option === 'allow-multiline') {
|
|
if (!adjacent) {
|
|
report(context, messages.afterOpenNoSpace, 'afterOpenNoSpace', {
|
|
node,
|
|
loc: {
|
|
start: openingToken.loc.start,
|
|
end: node.name.loc.start,
|
|
},
|
|
fix(fixer) {
|
|
return fixer.removeRange([openingToken.range[1], node.name.range[0]]);
|
|
},
|
|
});
|
|
}
|
|
} else if (option === 'always' && adjacent) {
|
|
report(context, messages.afterOpenNeedSpace, 'afterOpenNeedSpace', {
|
|
node,
|
|
loc: {
|
|
start: openingToken.loc.start,
|
|
end: node.name.loc.start,
|
|
},
|
|
fix(fixer) {
|
|
return fixer.insertTextBefore(node.name, ' ');
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
function validateBeforeClosing(context, node, option) {
|
|
// Don't enforce this rule for self closing tags
|
|
if (!node.selfClosing) {
|
|
const sourceCode = context.getSourceCode();
|
|
const leftToken = option === 'proportional-always'
|
|
? getTokenBeforeClosingBracket(node)
|
|
: sourceCode.getLastTokens(node, 2)[0];
|
|
const closingToken = sourceCode.getTokenAfter(leftToken);
|
|
|
|
if (node.loc.start.line !== node.loc.end.line && option === 'proportional-always') {
|
|
if (leftToken.loc.end.line === closingToken.loc.start.line) {
|
|
report(context, messages.beforeCloseNeedNewline, 'beforeCloseNeedNewline', {
|
|
node,
|
|
loc: leftToken.loc.end,
|
|
fix(fixer) {
|
|
return fixer.insertTextBefore(closingToken, '\n');
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (leftToken.loc.start.line !== closingToken.loc.start.line) {
|
|
return;
|
|
}
|
|
|
|
const adjacent = !sourceCode.isSpaceBetweenTokens(leftToken, closingToken);
|
|
|
|
if (option === 'never' && !adjacent) {
|
|
report(context, messages.beforeCloseNoSpace, 'beforeCloseNoSpace', {
|
|
node,
|
|
loc: {
|
|
start: leftToken.loc.end,
|
|
end: closingToken.loc.start,
|
|
},
|
|
fix(fixer) {
|
|
return fixer.removeRange([leftToken.range[1], closingToken.range[0]]);
|
|
},
|
|
});
|
|
} else if (option === 'always' && adjacent) {
|
|
report(context, messages.beforeCloseNeedSpace, 'beforeCloseNeedSpace', {
|
|
node,
|
|
loc: {
|
|
start: leftToken.loc.end,
|
|
end: closingToken.loc.start,
|
|
},
|
|
fix(fixer) {
|
|
return fixer.insertTextBefore(closingToken, ' ');
|
|
},
|
|
});
|
|
} else if (option === 'proportional-always' && node.type === 'JSXOpeningElement' && adjacent !== (node.loc.start.line === node.loc.end.line)) {
|
|
report(context, messages.beforeCloseNeedSpace, 'beforeCloseNeedSpace', {
|
|
node,
|
|
loc: {
|
|
start: leftToken.loc.end,
|
|
end: closingToken.loc.start,
|
|
},
|
|
fix(fixer) {
|
|
return fixer.insertTextBefore(closingToken, ' ');
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------
|
|
// Rule Definition
|
|
// ------------------------------------------------------------------------------
|
|
|
|
const optionDefaults = {
|
|
closingSlash: 'never',
|
|
beforeSelfClosing: 'always',
|
|
afterOpening: 'never',
|
|
beforeClosing: 'allow',
|
|
};
|
|
|
|
module.exports = {
|
|
meta: {
|
|
docs: {
|
|
description: 'Enforce whitespace in and around the JSX opening and closing brackets',
|
|
category: 'Stylistic Issues',
|
|
recommended: false,
|
|
url: docsUrl('jsx-tag-spacing'),
|
|
},
|
|
fixable: 'whitespace',
|
|
|
|
messages,
|
|
|
|
schema: [
|
|
{
|
|
type: 'object',
|
|
properties: {
|
|
closingSlash: {
|
|
enum: ['always', 'never', 'allow'],
|
|
},
|
|
beforeSelfClosing: {
|
|
enum: ['always', 'proportional-always', 'never', 'allow'],
|
|
},
|
|
afterOpening: {
|
|
enum: ['always', 'allow-multiline', 'never', 'allow'],
|
|
},
|
|
beforeClosing: {
|
|
enum: ['always', 'proportional-always', 'never', 'allow'],
|
|
},
|
|
},
|
|
default: optionDefaults,
|
|
additionalProperties: false,
|
|
},
|
|
],
|
|
},
|
|
create(context) {
|
|
const options = Object.assign({}, optionDefaults, context.options[0]);
|
|
|
|
return {
|
|
JSXOpeningElement(node) {
|
|
if (options.closingSlash !== 'allow' && node.selfClosing) {
|
|
validateClosingSlash(context, node, options.closingSlash);
|
|
}
|
|
if (options.afterOpening !== 'allow') {
|
|
validateAfterOpening(context, node, options.afterOpening);
|
|
}
|
|
if (options.beforeSelfClosing !== 'allow' && node.selfClosing) {
|
|
validateBeforeSelfClosing(context, node, options.beforeSelfClosing);
|
|
}
|
|
if (options.beforeClosing !== 'allow') {
|
|
validateBeforeClosing(context, node, options.beforeClosing);
|
|
}
|
|
},
|
|
JSXClosingElement(node) {
|
|
if (options.afterOpening !== 'allow') {
|
|
validateAfterOpening(context, node, options.afterOpening);
|
|
}
|
|
if (options.closingSlash !== 'allow') {
|
|
validateClosingSlash(context, node, options.closingSlash);
|
|
}
|
|
if (options.beforeClosing !== 'allow') {
|
|
validateBeforeClosing(context, node, options.beforeClosing);
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|