Skip to content

Commit

Permalink
Add jsx-tag-spacing rule (Fixes jsx-eslint#693)
Browse files Browse the repository at this point in the history
  • Loading branch information
Diogo Franco (Kovensky) committed Oct 24, 2016
1 parent 99cec3e commit 6c2ae9b
Show file tree
Hide file tree
Showing 3 changed files with 560 additions and 1 deletion.
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ var rules = {
'no-danger-with-children': require('./lib/rules/no-danger-with-children'),
'style-prop-object': require('./lib/rules/style-prop-object'),
'no-unused-prop-types': require('./lib/rules/no-unused-prop-types'),
'no-children-prop': require('./lib/rules/no-children-prop')
'no-children-prop': require('./lib/rules/no-children-prop'),
'jsx-tag-spacing': require('./lib/rules/jsx-tag-spacing')
};

var ruleNames = Object.keys(rules);
Expand Down
250 changes: 250 additions & 0 deletions lib/rules/jsx-tag-spacing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
/**
* @fileoverview Validates whitespace in and around the JSX opening and closing brackets
* @author Diogo Franco (Kovensky)
*/
'use strict';

// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------

/**
* Find the token before the closing bracket.
* @param {ASTNode} node - The JSX element node.
* @returns {Token} The token before the closing bracket.
*/
function getTokenBeforeClosingBracket(node) {
var attributes = node.attributes;
if (attributes.length === 0) {
return node.name;
}
return attributes[ attributes.length - 1 ];
}

// ------------------------------------------------------------------------------
// Validators
// ------------------------------------------------------------------------------

function validateClosingSlash(context, node, option) {
var sourceCode = context.getSourceCode();

var SELF_CLOSING_NEVER_MESSAGE = 'Whitespace is forbidden between `/` and `>`';
var SELF_CLOSING_ALWAYS_MESSAGE = 'Whitespace is required between `/` and `>`';
var NEVER_MESSAGE = 'Whitespace is forbidden between `<` and `/`';
var ALWAYS_MESSAGE = 'Whitespace is required between `<` and `/`';

var adjacent;

if (node.selfClosing) {
var lastTokens = sourceCode.getLastTokens(node, 2);

adjacent = !sourceCode.isSpaceBetweenTokens(lastTokens[0], lastTokens[1]);

if (option === 'never') {
if (!adjacent) {
context.report({
node: node,
loc: {
start: lastTokens[0].loc.start,
end: lastTokens[1].loc.end
},
message: SELF_CLOSING_NEVER_MESSAGE,
fix: function(fixer) {
return fixer.removeRange([lastTokens[0].range[1], lastTokens[1].range[0]]);
}
});
}
} else if (option === 'always' && adjacent) {
context.report({
node: node,
loc: {
start: lastTokens[0].loc.start,
end: lastTokens[1].loc.end
},
message: SELF_CLOSING_ALWAYS_MESSAGE,
fix: function(fixer) {
return fixer.insertTextBefore(lastTokens[1], ' ');
}
});
}
} else {
var firstTokens = sourceCode.getFirstTokens(node, 2);

adjacent = !sourceCode.isSpaceBetweenTokens(firstTokens[0], firstTokens[1]);

if (option === 'never') {
if (!adjacent) {
context.report({
node: node,
loc: {
start: firstTokens[0].loc.start,
end: firstTokens[1].loc.end
},
message: NEVER_MESSAGE,
fix: function(fixer) {
return fixer.removeRange([firstTokens[0].range[1], firstTokens[1].range[0]]);
}
});
}
} else if (option === 'always' && adjacent) {
context.report({
node: node,
loc: {
start: firstTokens[0].loc.start,
end: firstTokens[1].loc.end
},
message: ALWAYS_MESSAGE,
fix: function(fixer) {
return fixer.insertTextBefore(firstTokens[1], ' ');
}
});
}
}
}

function validateBeforeSelfClosing(context, node, option) {
var sourceCode = context.getSourceCode();

var NEVER_MESSAGE = 'A space is forbidden before closing bracket';
var ALWAYS_MESSAGE = 'A space is required before closing bracket';

var leftToken = getTokenBeforeClosingBracket(node);
var closingSlash = sourceCode.getTokenAfter(leftToken);

if (leftToken.loc.end.line !== closingSlash.loc.start.line) {
return;
}

if (option === 'always' && !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
context.report({
node: node,
loc: closingSlash.loc.start,
message: ALWAYS_MESSAGE,
fix: function(fixer) {
return fixer.insertTextBefore(closingSlash, ' ');
}
});
} else if (option === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
context.report({
node: node,
loc: closingSlash.loc.start,
message: NEVER_MESSAGE,
fix: function(fixer) {
var previousToken = sourceCode.getTokenBefore(closingSlash);
return fixer.removeRange([previousToken.range[1], closingSlash.range[0]]);
}
});
}
}

function validateAfterOpening(context, node, option) {
var sourceCode = context.getSourceCode();

var NEVER_MESSAGE = 'A space is forbidden after opening bracket';
var ALWAYS_MESSAGE = 'A space is required after opening bracket';

var openingToken = sourceCode.getTokenBefore(node.name);

if (option === 'allow-multiline') {
if (openingToken.loc.start.line !== node.name.loc.start.line) {
return;
}
}

var adjacent = !sourceCode.isSpaceBetweenTokens(openingToken, node.name);

if (option === 'never' || option === 'allow-multiline') {
if (!adjacent) {
context.report({
node: node,
loc: {
start: openingToken.loc.start,
end: node.name.loc.start
},
message: NEVER_MESSAGE,
fix: function(fixer) {
return fixer.removeRange([openingToken.range[1], node.name.range[0]]);
}
});
}
} else if (option === 'always' && adjacent) {
context.report({
node: node,
loc: {
start: openingToken.loc.start,
end: node.name.loc.start
},
message: ALWAYS_MESSAGE,
fix: function(fixer) {
return fixer.insertTextBefore(node.name, ' ');
}
});
}
}

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
docs: {},
fixable: 'whitespace',
schema: [
{
type: 'object',
properties: {
closingSlash: {
enum: ['always', 'never', 'allow']
},
beforeSelfClosing: {
enum: ['always', 'never', 'allow']
},
afterOpening: {
enum: ['always', 'allow-multiline', 'never', 'allow']
}
},
default: {
closingSlash: 'never',
beforeSelfClosing: 'always',
afterOpening: 'never'
},
additionalProperties: false
}
]
},
create: function (context) {
var options = {
closingSlash: 'never',
beforeSelfClosing: 'always',
afterOpening: 'never'
};
for (var key in options) {
if (options.hasOwnProperty(key) && context.options[0].hasOwnProperty(key)) {
options[key] = context.options[0][key];
}
}

return {
JSXOpeningElement: function (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);
}
},
JSXClosingElement: function (node) {
if (options.afterOpening !== 'allow') {
validateAfterOpening(context, node, options.afterOpening);
}
if (options.closingSlash !== 'allow') {
validateClosingSlash(context, node, options.closingSlash);
}
}
};
}
};
Loading

0 comments on commit 6c2ae9b

Please sign in to comment.