@stylistic/eslint-plugin-jsx
Advanced tools
Comparing version 1.1.0 to 1.2.0
@@ -21,30 +21,25 @@ 'use strict'; | ||
var jsxWrapMultilines = require('./jsx-wrap-multilines.js'); | ||
var configs = require('./configs.js'); | ||
require('./utils.js'); | ||
require('estraverse'); | ||
/** | ||
* This file is GENERATED by scripts/prepare.ts | ||
* DO NOT EDIT THIS FILE DIRECTLY | ||
*/ | ||
var rules = { | ||
'jsx-child-element-spacing': jsxChildElementSpacing.jsxChildElementSpacing, | ||
'jsx-closing-bracket-location': jsxClosingBracketLocation.jsxClosingBracketLocation, | ||
'jsx-closing-tag-location': jsxClosingTagLocation.jsxClosingTagLocation, | ||
'jsx-curly-brace-presence': jsxCurlyBracePresence.jsxCurlyBracePresence, | ||
'jsx-curly-newline': jsxCurlyNewline.jsxCurlyNewline, | ||
'jsx-curly-spacing': jsxCurlySpacing.jsxCurlySpacing, | ||
'jsx-equals-spacing': jsxEqualsSpacing.jsxEqualsSpacing, | ||
'jsx-first-prop-new-line': jsxFirstPropNewLine.jsxFirstPropNewLine, | ||
'jsx-indent': jsxIndent.jsxIndent, | ||
'jsx-indent-props': jsxIndentProps.jsxIndentProps, | ||
'jsx-max-props-per-line': jsxMaxPropsPerLine.jsxMaxPropsPerLine, | ||
'jsx-newline': jsxNewline.jsxNewline, | ||
'jsx-one-expression-per-line': jsxOneExpressionPerLine.jsxOneExpressionPerLine, | ||
'jsx-props-no-multi-spaces': jsxPropsNoMultiSpaces.jsxPropsNoMultiSpaces, | ||
'jsx-self-closing-comp': jsxSelfClosingComp.jsxSelfClosingComp, | ||
'jsx-sort-props': jsxSortProps.jsxSortProps, | ||
'jsx-tag-spacing': jsxTagSpacing.jsxTagSpacing, | ||
'jsx-wrap-multilines': jsxWrapMultilines.jsxWrapMultilines, | ||
"jsx-child-element-spacing": jsxChildElementSpacing.jsxChildElementSpacing, | ||
"jsx-closing-bracket-location": jsxClosingBracketLocation.jsxClosingBracketLocation, | ||
"jsx-closing-tag-location": jsxClosingTagLocation.jsxClosingTagLocation, | ||
"jsx-curly-brace-presence": jsxCurlyBracePresence.jsxCurlyBracePresence, | ||
"jsx-curly-newline": jsxCurlyNewline.jsxCurlyNewline, | ||
"jsx-curly-spacing": jsxCurlySpacing.jsxCurlySpacing, | ||
"jsx-equals-spacing": jsxEqualsSpacing.jsxEqualsSpacing, | ||
"jsx-first-prop-new-line": jsxFirstPropNewLine.jsxFirstPropNewLine, | ||
"jsx-indent": jsxIndent.jsxIndent, | ||
"jsx-indent-props": jsxIndentProps.jsxIndentProps, | ||
"jsx-max-props-per-line": jsxMaxPropsPerLine.jsxMaxPropsPerLine, | ||
"jsx-newline": jsxNewline.jsxNewline, | ||
"jsx-one-expression-per-line": jsxOneExpressionPerLine.jsxOneExpressionPerLine, | ||
"jsx-props-no-multi-spaces": jsxPropsNoMultiSpaces.jsxPropsNoMultiSpaces, | ||
"jsx-self-closing-comp": jsxSelfClosingComp.jsxSelfClosingComp, | ||
"jsx-sort-props": jsxSortProps.jsxSortProps, | ||
"jsx-tag-spacing": jsxTagSpacing.jsxTagSpacing, | ||
"jsx-wrap-multilines": jsxWrapMultilines.jsxWrapMultilines | ||
}; | ||
@@ -54,4 +49,5 @@ | ||
rules, | ||
configs: configs.configs | ||
}; | ||
module.exports = index; |
@@ -5,56 +5,47 @@ 'use strict'; | ||
// This list is taken from https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements | ||
// Note: 'br' is not included because whitespace around br tags is inconsequential to the rendered output | ||
const INLINE_ELEMENTS = new Set([ | ||
'a', | ||
'abbr', | ||
'acronym', | ||
'b', | ||
'bdo', | ||
'big', | ||
'button', | ||
'cite', | ||
'code', | ||
'dfn', | ||
'em', | ||
'i', | ||
'img', | ||
'input', | ||
'kbd', | ||
'label', | ||
'map', | ||
'object', | ||
'q', | ||
'samp', | ||
'script', | ||
'select', | ||
'small', | ||
'span', | ||
'strong', | ||
'sub', | ||
'sup', | ||
'textarea', | ||
'tt', | ||
'var', | ||
const INLINE_ELEMENTS = /* @__PURE__ */ new Set([ | ||
"a", | ||
"abbr", | ||
"acronym", | ||
"b", | ||
"bdo", | ||
"big", | ||
"button", | ||
"cite", | ||
"code", | ||
"dfn", | ||
"em", | ||
"i", | ||
"img", | ||
"input", | ||
"kbd", | ||
"label", | ||
"map", | ||
"object", | ||
"q", | ||
"samp", | ||
"script", | ||
"select", | ||
"small", | ||
"span", | ||
"strong", | ||
"sub", | ||
"sup", | ||
"textarea", | ||
"tt", | ||
"var" | ||
]); | ||
const messages = { | ||
spacingAfterPrev: 'Ambiguous spacing after previous element {{element}}', | ||
spacingBeforeNext: 'Ambiguous spacing before next element {{element}}', | ||
spacingAfterPrev: "Ambiguous spacing after previous element {{element}}", | ||
spacingBeforeNext: "Ambiguous spacing before next element {{element}}" | ||
}; | ||
var jsxChildElementSpacing = { | ||
var jsxChildElementSpacing = utils.createRule({ | ||
meta: { | ||
type: "layout", | ||
docs: { | ||
description: 'Enforce or disallow spaces inside of curly braces in JSX attributes and expressions', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('jsx-child-element-spacing'), | ||
description: "Enforce or disallow spaces inside of curly braces in JSX attributes and expressions", | ||
url: utils.docsUrl("jsx-child-element-spacing") | ||
}, | ||
fixable: null, | ||
messages, | ||
schema: [], | ||
schema: [] | ||
}, | ||
@@ -64,42 +55,26 @@ create(context) { | ||
const TEXT_PRECEDING_ELEMENT_PATTERN = /\S\s*\n\s*$/; | ||
const elementName = node => ( | ||
node.openingElement | ||
&& node.openingElement.name | ||
&& node.openingElement.name.type === 'JSXIdentifier' | ||
&& node.openingElement.name.name | ||
); | ||
const isInlineElement = node => ( | ||
node.type === 'JSXElement' | ||
&& INLINE_ELEMENTS.has(elementName(node)) | ||
); | ||
const elementName = (node) => node.openingElement && node.openingElement.name && node.openingElement.name.type === "JSXIdentifier" && node.openingElement.name.name || ""; | ||
const isInlineElement = (node) => node.type === "JSXElement" && INLINE_ELEMENTS.has(elementName(node)); | ||
const handleJSX = (node) => { | ||
let lastChild = null; | ||
let child = null; | ||
(node.children.concat([null])).forEach((nextChild) => { | ||
if ( | ||
(lastChild || nextChild) | ||
&& (!lastChild || isInlineElement(lastChild)) | ||
&& (child && (child.type === 'Literal' || child.type === 'JSXText')) | ||
&& (!nextChild || isInlineElement(nextChild)) | ||
&& true | ||
) { | ||
if (lastChild && child.value.match(TEXT_FOLLOWING_ELEMENT_PATTERN)) { | ||
utils.report(context, messages.spacingAfterPrev, 'spacingAfterPrev', { | ||
[...node.children, null].forEach((nextChild) => { | ||
if ((lastChild || nextChild) && (!lastChild || isInlineElement(lastChild)) && (child && (child.type === "Literal" || child.type === "JSXText")) && (!nextChild || isInlineElement(nextChild)) && true) { | ||
if (lastChild && String(child.value).match(TEXT_FOLLOWING_ELEMENT_PATTERN)) { | ||
context.report({ | ||
messageId: "spacingAfterPrev", | ||
node: lastChild, | ||
loc: lastChild.loc.end, | ||
data: { | ||
element: elementName(lastChild), | ||
}, | ||
element: elementName(lastChild) | ||
} | ||
}); | ||
} | ||
else if (nextChild && child.value.match(TEXT_PRECEDING_ELEMENT_PATTERN)) { | ||
utils.report(context, messages.spacingBeforeNext, 'spacingBeforeNext', { | ||
} else if (nextChild && String(child.value).match(TEXT_PRECEDING_ELEMENT_PATTERN)) { | ||
context.report({ | ||
messageId: "spacingBeforeNext", | ||
node: nextChild, | ||
loc: nextChild.loc.start, | ||
data: { | ||
element: elementName(nextChild), | ||
}, | ||
element: elementName(nextChild) | ||
} | ||
}); | ||
@@ -112,10 +87,9 @@ } | ||
}; | ||
return { | ||
JSXElement: handleJSX, | ||
JSXFragment: handleJSX, | ||
} | ||
}, | ||
}; | ||
JSXFragment: handleJSX | ||
}; | ||
} | ||
}); | ||
exports.jsxChildElementSpacing = jsxChildElementSpacing; |
@@ -5,166 +5,109 @@ 'use strict'; | ||
/** | ||
* @fileoverview Validate closing bracket location in JSX | ||
* @author Yannick Croissant | ||
*/ | ||
const has = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
const messages = { | ||
bracketLocation: 'The closing bracket must be {{location}}{{details}}', | ||
bracketLocation: "The closing bracket must be {{location}}{{details}}" | ||
}; | ||
var jsxClosingBracketLocation = { | ||
meta: { | ||
docs: { | ||
description: 'Enforce closing bracket location in JSX', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('jsx-closing-bracket-location'), | ||
description: "Enforce closing bracket location in JSX", | ||
category: "Stylistic Issues", | ||
url: utils.docsUrl("jsx-closing-bracket-location") | ||
}, | ||
fixable: 'code', | ||
fixable: "code", | ||
messages, | ||
schema: [{ | ||
anyOf: [ | ||
{ | ||
enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned'], | ||
enum: ["after-props", "props-aligned", "tag-aligned", "line-aligned"] | ||
}, | ||
{ | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
location: { | ||
enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned'], | ||
}, | ||
enum: ["after-props", "props-aligned", "tag-aligned", "line-aligned"] | ||
} | ||
}, | ||
additionalProperties: false, | ||
additionalProperties: false | ||
}, | ||
{ | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
nonEmpty: { | ||
enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned', false], | ||
enum: ["after-props", "props-aligned", "tag-aligned", "line-aligned", false] | ||
}, | ||
selfClosing: { | ||
enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned', false], | ||
}, | ||
enum: ["after-props", "props-aligned", "tag-aligned", "line-aligned", false] | ||
} | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
}], | ||
additionalProperties: false | ||
} | ||
] | ||
}] | ||
}, | ||
create(context) { | ||
const MESSAGE_LOCATION = { | ||
'after-props': 'placed after the last prop', | ||
'after-tag': 'placed after the opening tag', | ||
'props-aligned': 'aligned with the last prop', | ||
'tag-aligned': 'aligned with the opening tag', | ||
'line-aligned': 'aligned with the line containing the opening tag', | ||
"after-props": "placed after the last prop", | ||
"after-tag": "placed after the opening tag", | ||
"props-aligned": "aligned with the last prop", | ||
"tag-aligned": "aligned with the opening tag", | ||
"line-aligned": "aligned with the line containing the opening tag" | ||
}; | ||
const DEFAULT_LOCATION = 'tag-aligned'; | ||
const DEFAULT_LOCATION = "tag-aligned"; | ||
const config = context.options[0]; | ||
const options = { | ||
nonEmpty: DEFAULT_LOCATION, | ||
selfClosing: DEFAULT_LOCATION, | ||
selfClosing: DEFAULT_LOCATION | ||
}; | ||
if (typeof config === 'string') { | ||
// simple shorthand [1, 'something'] | ||
if (typeof config === "string") { | ||
options.nonEmpty = config; | ||
options.selfClosing = config; | ||
} | ||
else if (typeof config === 'object') { | ||
// [1, {location: 'something'}] (back-compat) | ||
if (has(config, 'location')) { | ||
} else if (typeof config === "object") { | ||
if (has(config, "location")) { | ||
options.nonEmpty = config.location; | ||
options.selfClosing = config.location; | ||
} | ||
// [1, {nonEmpty: 'something'}] | ||
if (has(config, 'nonEmpty')) | ||
if (has(config, "nonEmpty")) | ||
options.nonEmpty = config.nonEmpty; | ||
// [1, {selfClosing: 'something'}] | ||
if (has(config, 'selfClosing')) | ||
if (has(config, "selfClosing")) | ||
options.selfClosing = config.selfClosing; | ||
} | ||
/** | ||
* Get expected location for the closing bracket | ||
* @param {object} tokens Locations of the opening bracket, closing bracket and last prop | ||
* @return {string} Expected location for the closing bracket | ||
*/ | ||
function getExpectedLocation(tokens) { | ||
let location; | ||
// Is always after the opening tag if there is no props | ||
if (typeof tokens.lastProp === 'undefined') | ||
location = 'after-tag'; | ||
// Is always after the last prop if this one is on the same line as the opening bracket | ||
if (typeof tokens.lastProp === "undefined") | ||
location = "after-tag"; | ||
else if (tokens.opening.line === tokens.lastProp.lastLine) | ||
location = 'after-props'; | ||
// Else use configuration dependent on selfClosing property | ||
location = "after-props"; | ||
else | ||
location = tokens.selfClosing ? options.selfClosing : options.nonEmpty; | ||
return location | ||
return location; | ||
} | ||
/** | ||
* Get the correct 0-indexed column for the closing bracket, given the | ||
* expected location. | ||
* @param {object} tokens Locations of the opening bracket, closing bracket and last prop | ||
* @param {string} expectedLocation Expected location for the closing bracket | ||
* @return {?number} The correct column for the closing bracket, or null | ||
*/ | ||
function getCorrectColumn(tokens, expectedLocation) { | ||
switch (expectedLocation) { | ||
case 'props-aligned': | ||
return tokens.lastProp.column | ||
case 'tag-aligned': | ||
return tokens.opening.column | ||
case 'line-aligned': | ||
return tokens.openingStartOfLine.column | ||
case "props-aligned": | ||
return tokens.lastProp.column; | ||
case "tag-aligned": | ||
return tokens.opening.column; | ||
case "line-aligned": | ||
return tokens.openingStartOfLine.column; | ||
default: | ||
return null | ||
return null; | ||
} | ||
} | ||
/** | ||
* Check if the closing bracket is correctly located | ||
* @param {object} tokens Locations of the opening bracket, closing bracket and last prop | ||
* @param {string} expectedLocation Expected location for the closing bracket | ||
* @return {boolean} True if the closing bracket is correctly located, false if not | ||
*/ | ||
function hasCorrectLocation(tokens, expectedLocation) { | ||
switch (expectedLocation) { | ||
case 'after-tag': | ||
return tokens.tag.line === tokens.closing.line | ||
case 'after-props': | ||
return tokens.lastProp.lastLine === tokens.closing.line | ||
case 'props-aligned': | ||
case 'tag-aligned': | ||
case 'line-aligned': { | ||
case "after-tag": | ||
return tokens.tag.line === tokens.closing.line; | ||
case "after-props": | ||
return tokens.lastProp.lastLine === tokens.closing.line; | ||
case "props-aligned": | ||
case "tag-aligned": | ||
case "line-aligned": { | ||
const correctColumn = getCorrectColumn(tokens, expectedLocation); | ||
return correctColumn === tokens.closing.column | ||
return correctColumn === tokens.closing.column; | ||
} | ||
default: | ||
return true | ||
return true; | ||
} | ||
} | ||
/** | ||
* Get the characters used for indentation on the line to be matched | ||
* @param {object} tokens Locations of the opening bracket, closing bracket and last prop | ||
* @param {string} expectedLocation Expected location for the closing bracket | ||
* @param {number} [correctColumn] Expected column for the closing bracket. Default to 0 | ||
* @return {string} The characters used for indentation | ||
*/ | ||
function getIndentation(tokens, expectedLocation, correctColumn) { | ||
@@ -175,26 +118,17 @@ const newColumn = correctColumn || 0; | ||
switch (expectedLocation) { | ||
case 'props-aligned': | ||
case "props-aligned": | ||
indentation = /^\s*/.exec(context.getSourceCode().lines[tokens.lastProp.firstLine - 1])[0]; | ||
break | ||
case 'tag-aligned': | ||
case 'line-aligned': | ||
break; | ||
case "tag-aligned": | ||
case "line-aligned": | ||
indentation = /^\s*/.exec(context.getSourceCode().lines[tokens.opening.line - 1])[0]; | ||
break | ||
break; | ||
default: | ||
indentation = ''; | ||
indentation = ""; | ||
} | ||
if (indentation.length + 1 < newColumn) { | ||
// Non-whitespace characters were included in the column offset | ||
spaces = Array.from({ length: +correctColumn + 1 - indentation.length }); | ||
} | ||
return indentation + spaces.join(' ') | ||
return indentation + spaces.join(" "); | ||
} | ||
/** | ||
* Get the locations of the opening bracket, closing bracket, last prop, and | ||
* start of opening line. | ||
* @param {ASTNode} node The node to check | ||
* @return {object} Locations of the opening bracket, closing bracket, last | ||
* prop and start of opening line. | ||
*/ | ||
function getTokensLocations(node) { | ||
@@ -211,3 +145,3 @@ const sourceCode = context.getSourceCode(); | ||
firstLine: sourceCode.getFirstToken(lastProp).loc.start.line, | ||
lastLine: sourceCode.getLastToken(lastProp).loc.end.line, | ||
lastLine: sourceCode.getLastToken(lastProp).loc.end.line | ||
}; | ||
@@ -219,7 +153,7 @@ } | ||
openTab: /^\t/.test(openingLine), | ||
closeTab: /^\t/.test(closingLine), | ||
closeTab: /^\t/.test(closingLine) | ||
}; | ||
const openingStartOfLine = { | ||
column: /^\s*/.exec(openingLine)[0].length, | ||
line: opening.line, | ||
line: opening.line | ||
}; | ||
@@ -233,18 +167,9 @@ return { | ||
selfClosing: node.selfClosing, | ||
openingStartOfLine, | ||
} | ||
openingStartOfLine | ||
}; | ||
} | ||
/** | ||
* Get an unique ID for a given JSXOpeningElement | ||
* | ||
* @param {ASTNode} node The AST node being checked. | ||
* @returns {string} Unique ID (based on its range) | ||
*/ | ||
function getOpeningElementId(node) { | ||
return node.range.join(':') | ||
return node.range.join(":"); | ||
} | ||
const lastAttributeNode = {}; | ||
return { | ||
@@ -254,11 +179,8 @@ JSXAttribute(node) { | ||
}, | ||
JSXSpreadAttribute(node) { | ||
lastAttributeNode[getOpeningElementId(node.parent)] = node; | ||
}, | ||
'JSXOpeningElement:exit': function (node) { | ||
"JSXOpeningElement:exit": function(node) { | ||
const attributeNode = lastAttributeNode[getOpeningElementId(node)]; | ||
const cachedLastAttributeEndPos = attributeNode ? attributeNode.range[1] : null; | ||
let expectedNextLine; | ||
@@ -268,19 +190,13 @@ const tokens = getTokensLocations(node); | ||
let usingSameIndentation = true; | ||
if (expectedLocation === 'tag-aligned') | ||
if (expectedLocation === "tag-aligned") | ||
usingSameIndentation = tokens.isTab.openTab === tokens.isTab.closeTab; | ||
if (hasCorrectLocation(tokens, expectedLocation) && usingSameIndentation) | ||
return | ||
return; | ||
const data = { location: MESSAGE_LOCATION[expectedLocation] }; | ||
const correctColumn = getCorrectColumn(tokens, expectedLocation); | ||
if (correctColumn !== null) { | ||
expectedNextLine = tokens.lastProp | ||
&& (tokens.lastProp.lastLine === tokens.closing.line); | ||
data.details = ` (expected column ${correctColumn + 1}${expectedNextLine ? ' on the next line)' : ')'}`; | ||
expectedNextLine = tokens.lastProp && tokens.lastProp.lastLine === tokens.closing.line; | ||
data.details = ` (expected column ${correctColumn + 1}${expectedNextLine ? " on the next line)" : ")"}`; | ||
} | ||
utils.report(context, messages.bracketLocation, 'bracketLocation', { | ||
utils.report(context, messages.bracketLocation, "bracketLocation", { | ||
node, | ||
@@ -290,25 +206,25 @@ loc: tokens.closing, | ||
fix(fixer) { | ||
const closingTag = tokens.selfClosing ? '/>' : '>'; | ||
const closingTag = tokens.selfClosing ? "/>" : ">"; | ||
switch (expectedLocation) { | ||
case 'after-tag': | ||
case "after-tag": | ||
if (cachedLastAttributeEndPos) | ||
return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]], (expectedNextLine ? '\n' : '') + closingTag) | ||
return fixer.replaceTextRange([node.name.range[1], node.range[1]], (expectedNextLine ? '\n' : ' ') + closingTag) | ||
case 'after-props': | ||
return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]], (expectedNextLine ? '\n' : '') + closingTag) | ||
case 'props-aligned': | ||
case 'tag-aligned': | ||
case 'line-aligned': | ||
return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]], `\n${getIndentation(tokens, expectedLocation, correctColumn)}${closingTag}`) | ||
return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]], (expectedNextLine ? "\n" : "") + closingTag); | ||
return fixer.replaceTextRange([node.name.range[1], node.range[1]], (expectedNextLine ? "\n" : " ") + closingTag); | ||
case "after-props": | ||
return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]], (expectedNextLine ? "\n" : "") + closingTag); | ||
case "props-aligned": | ||
case "tag-aligned": | ||
case "line-aligned": | ||
return fixer.replaceTextRange([cachedLastAttributeEndPos, node.range[1]], ` | ||
${getIndentation(tokens, expectedLocation, correctColumn)}${closingTag}`); | ||
default: | ||
return true | ||
return true; | ||
} | ||
}, | ||
} | ||
}); | ||
}, | ||
} | ||
}, | ||
} | ||
}; | ||
} | ||
}; | ||
exports.jsxClosingBracketLocation = jsxClosingBracketLocation; |
@@ -5,44 +5,26 @@ 'use strict'; | ||
/** | ||
* @fileoverview Validate closing tag location in JSX | ||
* @author Ross Solomon | ||
*/ | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
const messages = { | ||
onOwnLine: 'Closing tag of a multiline JSX expression must be on its own line.', | ||
matchIndent: 'Expected closing tag to match indentation of opening.', | ||
onOwnLine: "Closing tag of a multiline JSX expression must be on its own line.", | ||
matchIndent: "Expected closing tag to match indentation of opening." | ||
}; | ||
var jsxClosingTagLocation = { | ||
meta: { | ||
docs: { | ||
description: 'Enforce closing tag location for multiline JSX', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('jsx-closing-tag-location'), | ||
description: "Enforce closing tag location for multiline JSX", | ||
category: "Stylistic Issues", | ||
url: utils.docsUrl("jsx-closing-tag-location") | ||
}, | ||
fixable: 'whitespace', | ||
messages, | ||
fixable: "whitespace", | ||
messages | ||
}, | ||
create(context) { | ||
function handleClosingElement(node) { | ||
if (!node.parent) | ||
return | ||
return; | ||
const opening = node.parent.openingElement || node.parent.openingFragment; | ||
if (opening.loc.start.line === node.loc.start.line) | ||
return | ||
return; | ||
if (opening.loc.start.column === node.loc.start.column) | ||
return | ||
const messageId = utils.isNodeFirstInLine(context, node) | ||
? 'matchIndent' | ||
: 'onOwnLine'; | ||
return; | ||
const messageId = utils.isNodeFirstInLine(context, node) ? "matchIndent" : "onOwnLine"; | ||
utils.report(context, messages[messageId], messageId, { | ||
@@ -52,22 +34,21 @@ node, | ||
fix(fixer) { | ||
const indent = Array(opening.loc.start.column + 1).join(' '); | ||
const indent = Array(opening.loc.start.column + 1).join(" "); | ||
if (utils.isNodeFirstInLine(context, node)) { | ||
return fixer.replaceTextRange( | ||
[node.range[0] - node.loc.start.column, node.range[0]], | ||
indent, | ||
) | ||
indent | ||
); | ||
} | ||
return fixer.insertTextBefore(node, `\n${indent}`) | ||
}, | ||
return fixer.insertTextBefore(node, ` | ||
${indent}`); | ||
} | ||
}); | ||
} | ||
return { | ||
JSXClosingElement: handleClosingElement, | ||
JSXClosingFragment: handleClosingElement, | ||
} | ||
}, | ||
JSXClosingFragment: handleClosingElement | ||
}; | ||
} | ||
}; | ||
exports.jsxClosingTagLocation = jsxClosingTagLocation; |
@@ -5,47 +5,25 @@ 'use strict'; | ||
/** | ||
* @fileoverview Enforce curly braces or disallow unnecessary curly brace in JSX | ||
* @author Jacky Ho | ||
* @author Simon Lydell | ||
*/ | ||
const arrayIncludes = (arr, value) => arr.includes(value); | ||
// ------------------------------------------------------------------------------ | ||
// Constants | ||
// ------------------------------------------------------------------------------ | ||
const OPTION_ALWAYS = 'always'; | ||
const OPTION_NEVER = 'never'; | ||
const OPTION_IGNORE = 'ignore'; | ||
const OPTION_ALWAYS = "always"; | ||
const OPTION_NEVER = "never"; | ||
const OPTION_IGNORE = "ignore"; | ||
const OPTION_VALUES = [ | ||
OPTION_ALWAYS, | ||
OPTION_NEVER, | ||
OPTION_IGNORE, | ||
OPTION_IGNORE | ||
]; | ||
const DEFAULT_CONFIG = { props: OPTION_NEVER, children: OPTION_NEVER, propElementValues: OPTION_IGNORE }; | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
const messages = { | ||
unnecessaryCurly: 'Curly braces are unnecessary here.', | ||
missingCurly: 'Need to wrap this literal in a JSX expression.', | ||
unnecessaryCurly: "Curly braces are unnecessary here.", | ||
missingCurly: "Need to wrap this literal in a JSX expression." | ||
}; | ||
var jsxCurlyBracePresence = { | ||
meta: { | ||
docs: { | ||
description: 'Disallow unnecessary JSX expressions when literals alone are sufficient or enforce JSX expressions on literals in JSX children or attributes', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('jsx-curly-brace-presence'), | ||
description: "Disallow unnecessary JSX expressions when literals alone are sufficient or enforce JSX expressions on literals in JSX children or attributes", | ||
category: "Stylistic Issues", | ||
url: utils.docsUrl("jsx-curly-brace-presence") | ||
}, | ||
fixable: 'code', | ||
fixable: "code", | ||
messages, | ||
schema: [ | ||
@@ -55,121 +33,86 @@ { | ||
{ | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
props: { enum: OPTION_VALUES }, | ||
children: { enum: OPTION_VALUES }, | ||
propElementValues: { enum: OPTION_VALUES }, | ||
propElementValues: { enum: OPTION_VALUES } | ||
}, | ||
additionalProperties: false, | ||
additionalProperties: false | ||
}, | ||
{ | ||
enum: OPTION_VALUES, | ||
}, | ||
], | ||
}, | ||
], | ||
enum: OPTION_VALUES | ||
} | ||
] | ||
} | ||
] | ||
}, | ||
create(context) { | ||
const HTML_ENTITY_REGEX = () => /&[A-Za-z\d#]+;/g; | ||
const ruleOptions = context.options[0]; | ||
const userConfig = typeof ruleOptions === 'string' | ||
? { props: ruleOptions, children: ruleOptions, propElementValues: OPTION_IGNORE } | ||
: Object.assign({}, DEFAULT_CONFIG, ruleOptions); | ||
const userConfig = typeof ruleOptions === "string" ? { props: ruleOptions, children: ruleOptions, propElementValues: OPTION_IGNORE } : Object.assign({}, DEFAULT_CONFIG, ruleOptions); | ||
function containsLineTerminators(rawStringValue) { | ||
return /[\n\r\u2028\u2029]/.test(rawStringValue) | ||
return /[\n\r\u2028\u2029]/.test(rawStringValue); | ||
} | ||
function containsBackslash(rawStringValue) { | ||
return arrayIncludes(rawStringValue, '\\') | ||
return arrayIncludes(rawStringValue, "\\"); | ||
} | ||
function containsHTMLEntity(rawStringValue) { | ||
return HTML_ENTITY_REGEX().test(rawStringValue) | ||
return HTML_ENTITY_REGEX().test(rawStringValue); | ||
} | ||
function containsOnlyHtmlEntities(rawStringValue) { | ||
return rawStringValue.replace(HTML_ENTITY_REGEX(), '').trim() === '' | ||
return rawStringValue.replace(HTML_ENTITY_REGEX(), "").trim() === ""; | ||
} | ||
function containsDisallowedJSXTextChars(rawStringValue) { | ||
return /[{<>}]/.test(rawStringValue) | ||
return /[{<>}]/.test(rawStringValue); | ||
} | ||
function containsQuoteCharacters(value) { | ||
return /['"]/.test(value) | ||
return /['"]/.test(value); | ||
} | ||
function containsMultilineComment(value) { | ||
return /\/\*/.test(value) | ||
return /\/\*/.test(value); | ||
} | ||
function escapeDoubleQuotes(rawStringValue) { | ||
return rawStringValue.replace(/\\"/g, '"').replace(/"/g, '\\"') | ||
return rawStringValue.replace(/\\"/g, '"').replace(/"/g, '\\"'); | ||
} | ||
function escapeBackslashes(rawStringValue) { | ||
return rawStringValue.replace(/\\/g, '\\\\') | ||
return rawStringValue.replace(/\\/g, "\\\\"); | ||
} | ||
function needToEscapeCharacterForJSX(raw, node) { | ||
return ( | ||
containsBackslash(raw) | ||
|| containsHTMLEntity(raw) | ||
|| (node.parent.type !== 'JSXAttribute' && containsDisallowedJSXTextChars(raw)) | ||
) | ||
return containsBackslash(raw) || containsHTMLEntity(raw) || node.parent.type !== "JSXAttribute" && containsDisallowedJSXTextChars(raw); | ||
} | ||
function containsWhitespaceExpression(child) { | ||
if (child.type === 'JSXExpressionContainer') { | ||
if (child.type === "JSXExpressionContainer") { | ||
const value = child.expression.value; | ||
return value ? utils.isWhiteSpaces(value) : false | ||
return value ? utils.isWhiteSpaces(value) : false; | ||
} | ||
return false | ||
return false; | ||
} | ||
function isLineBreak(text) { | ||
return containsLineTerminators(text) && text.trim() === '' | ||
return containsLineTerminators(text) && text.trim() === ""; | ||
} | ||
function wrapNonHTMLEntities(text) { | ||
const HTML_ENTITY = '<HTML_ENTITY>'; | ||
const withCurlyBraces = text.split(HTML_ENTITY_REGEX()).map(word => ( | ||
word === '' ? '' : `{${JSON.stringify(word)}}` | ||
)).join(HTML_ENTITY); | ||
const HTML_ENTITY = "<HTML_ENTITY>"; | ||
const withCurlyBraces = text.split(HTML_ENTITY_REGEX()).map((word) => word === "" ? "" : `{${JSON.stringify(word)}}`).join(HTML_ENTITY); | ||
const htmlEntities = text.match(HTML_ENTITY_REGEX()); | ||
return htmlEntities.reduce((acc, htmlEntity) => (acc.replace(HTML_ENTITY, htmlEntity) | ||
), withCurlyBraces) | ||
return htmlEntities.reduce((acc, htmlEntity) => acc.replace(HTML_ENTITY, htmlEntity), withCurlyBraces); | ||
} | ||
function wrapWithCurlyBraces(rawText) { | ||
if (!containsLineTerminators(rawText)) | ||
return `{${JSON.stringify(rawText)}}` | ||
return rawText.split('\n').map((line) => { | ||
if (line.trim() === '') | ||
return line | ||
return `{${JSON.stringify(rawText)}}`; | ||
return rawText.split("\n").map((line) => { | ||
if (line.trim() === "") | ||
return line; | ||
const firstCharIndex = line.search(/[^\s]/); | ||
const leftWhitespace = line.slice(0, firstCharIndex); | ||
const text = line.slice(firstCharIndex); | ||
if (containsHTMLEntity(line)) | ||
return `${leftWhitespace}${wrapNonHTMLEntities(text)}` | ||
return `${leftWhitespace}{${JSON.stringify(text)}}` | ||
}).join('\n') | ||
return `${leftWhitespace}${wrapNonHTMLEntities(text)}`; | ||
return `${leftWhitespace}{${JSON.stringify(text)}}`; | ||
}).join("\n"); | ||
} | ||
/** | ||
* Report and fix an unnecessary curly brace violation on a node | ||
* @param {ASTNode} JSXExpressionNode - The AST node with an unnecessary JSX expression | ||
*/ | ||
function reportUnnecessaryCurly(JSXExpressionNode) { | ||
utils.report(context, messages.unnecessaryCurly, 'unnecessaryCurly', { | ||
utils.report(context, messages.unnecessaryCurly, "unnecessaryCurly", { | ||
node: JSXExpressionNode, | ||
fix(fixer) { | ||
const expression = JSXExpressionNode.expression; | ||
let textToReplace; | ||
@@ -179,103 +122,51 @@ if (utils.isJSX(expression)) { | ||
textToReplace = sourceCode.getText(expression); | ||
} | ||
else { | ||
} else { | ||
const expressionType = expression && expression.type; | ||
const parentType = JSXExpressionNode.parent.type; | ||
if (parentType === 'JSXAttribute') { | ||
textToReplace = `"${expressionType === 'TemplateLiteral' | ||
? expression.quasis[0].value.raw | ||
: expression.raw.slice(1, -1) | ||
}"`; | ||
} | ||
else if (utils.isJSX(expression)) { | ||
if (parentType === "JSXAttribute") { | ||
textToReplace = `"${expressionType === "TemplateLiteral" ? expression.quasis[0].value.raw : expression.raw.slice(1, -1)}"`; | ||
} else if (utils.isJSX(expression)) { | ||
const sourceCode = context.getSourceCode(); | ||
textToReplace = sourceCode.getText(expression); | ||
} else { | ||
textToReplace = expressionType === "TemplateLiteral" ? expression.quasis[0].value.cooked : expression.value; | ||
} | ||
else { | ||
textToReplace = expressionType === 'TemplateLiteral' | ||
? expression.quasis[0].value.cooked : expression.value; | ||
} | ||
} | ||
return fixer.replaceText(JSXExpressionNode, textToReplace) | ||
}, | ||
return fixer.replaceText(JSXExpressionNode, textToReplace); | ||
} | ||
}); | ||
} | ||
function reportMissingCurly(literalNode) { | ||
utils.report(context, messages.missingCurly, 'missingCurly', { | ||
utils.report(context, messages.missingCurly, "missingCurly", { | ||
node: literalNode, | ||
fix(fixer) { | ||
if (utils.isJSX(literalNode)) | ||
return fixer.replaceText(literalNode, `{${context.getSourceCode().getText(literalNode)}}`) | ||
// If a HTML entity name is found, bail out because it can be fixed | ||
// by either using the real character or the unicode equivalent. | ||
// If it contains any line terminator character, bail out as well. | ||
if ( | ||
containsOnlyHtmlEntities(literalNode.raw) | ||
|| (literalNode.parent.type === 'JSXAttribute' && containsLineTerminators(literalNode.raw)) | ||
|| isLineBreak(literalNode.raw) | ||
) | ||
return null | ||
const expression = literalNode.parent.type === 'JSXAttribute' | ||
? `{"${escapeDoubleQuotes(escapeBackslashes( | ||
literalNode.raw.slice(1, -1), | ||
))}"}` | ||
: wrapWithCurlyBraces(literalNode.raw); | ||
return fixer.replaceText(literalNode, expression) | ||
}, | ||
return fixer.replaceText(literalNode, `{${context.getSourceCode().getText(literalNode)}}`); | ||
if (containsOnlyHtmlEntities(literalNode.raw) || literalNode.parent.type === "JSXAttribute" && containsLineTerminators(literalNode.raw) || isLineBreak(literalNode.raw)) | ||
return null; | ||
const expression = literalNode.parent.type === "JSXAttribute" ? `{"${escapeDoubleQuotes(escapeBackslashes( | ||
literalNode.raw.slice(1, -1) | ||
))}"}` : wrapWithCurlyBraces(literalNode.raw); | ||
return fixer.replaceText(literalNode, expression); | ||
} | ||
}); | ||
} | ||
function isWhiteSpaceLiteral(node) { | ||
return node.type && node.type === 'Literal' && node.value && utils.isWhiteSpaces(node.value) | ||
return node.type && node.type === "Literal" && node.value && utils.isWhiteSpaces(node.value); | ||
} | ||
function isStringWithTrailingWhiteSpaces(value) { | ||
return /^\s|\s$/.test(value) | ||
return /^\s|\s$/.test(value); | ||
} | ||
function isLiteralWithTrailingWhiteSpaces(node) { | ||
return node.type && node.type === 'Literal' && node.value && isStringWithTrailingWhiteSpaces(node.value) | ||
return node.type && node.type === "Literal" && node.value && isStringWithTrailingWhiteSpaces(node.value); | ||
} | ||
// Bail out if there is any character that needs to be escaped in JSX | ||
// because escaping decreases readability and the original code may be more | ||
// readable anyway or intentional for other specific reasons | ||
function lintUnnecessaryCurly(JSXExpressionNode) { | ||
const expression = JSXExpressionNode.expression; | ||
const expressionType = expression.type; | ||
const sourceCode = context.getSourceCode(); | ||
// Curly braces containing comments are necessary | ||
if (sourceCode.getCommentsInside && sourceCode.getCommentsInside(JSXExpressionNode).length > 0) | ||
return | ||
if ( | ||
(expressionType === 'Literal' || expressionType === 'JSXText') | ||
&& typeof expression.value === 'string' | ||
&& ( | ||
(JSXExpressionNode.parent.type === 'JSXAttribute' && !isWhiteSpaceLiteral(expression)) | ||
|| !isLiteralWithTrailingWhiteSpaces(expression) | ||
) | ||
&& !containsMultilineComment(expression.value) | ||
&& !needToEscapeCharacterForJSX(expression.raw, JSXExpressionNode) && ( | ||
utils.isJSX(JSXExpressionNode.parent) | ||
|| !containsQuoteCharacters(expression.value) | ||
) | ||
) | ||
return; | ||
if ((expressionType === "Literal" || expressionType === "JSXText") && typeof expression.value === "string" && (JSXExpressionNode.parent.type === "JSXAttribute" && !isWhiteSpaceLiteral(expression) || !isLiteralWithTrailingWhiteSpaces(expression)) && !containsMultilineComment(expression.value) && !needToEscapeCharacterForJSX(expression.raw, JSXExpressionNode) && (utils.isJSX(JSXExpressionNode.parent) || !containsQuoteCharacters(expression.value))) | ||
reportUnnecessaryCurly(JSXExpressionNode); | ||
else if ( | ||
expressionType === 'TemplateLiteral' | ||
&& expression.expressions.length === 0 | ||
&& !expression.quasis[0].value.raw.includes('\n') | ||
&& !isStringWithTrailingWhiteSpaces(expression.quasis[0].value.raw) | ||
&& !needToEscapeCharacterForJSX(expression.quasis[0].value.raw, JSXExpressionNode) | ||
&& !containsQuoteCharacters(expression.quasis[0].value.cooked) | ||
) | ||
else if (expressionType === "TemplateLiteral" && expression.expressions.length === 0 && !expression.quasis[0].value.raw.includes("\n") && !isStringWithTrailingWhiteSpaces(expression.quasis[0].value.raw) && !needToEscapeCharacterForJSX(expression.quasis[0].value.raw, JSXExpressionNode) && !containsQuoteCharacters(expression.quasis[0].value.cooked)) | ||
reportUnnecessaryCurly(JSXExpressionNode); | ||
@@ -285,15 +176,5 @@ else if (utils.isJSX(expression)) | ||
} | ||
function areRuleConditionsSatisfied(parent, config, ruleCondition) { | ||
return ( | ||
parent.type === 'JSXAttribute' | ||
&& typeof config.props === 'string' | ||
&& config.props === ruleCondition | ||
) || ( | ||
utils.isJSX(parent) | ||
&& typeof config.children === 'string' | ||
&& config.children === ruleCondition | ||
) | ||
return parent.type === "JSXAttribute" && typeof config.props === "string" && config.props === ruleCondition || utils.isJSX(parent) && typeof config.children === "string" && config.children === ruleCondition; | ||
} | ||
function getAdjacentSiblings(node, children) { | ||
@@ -303,95 +184,51 @@ for (let i = 1; i < children.length - 1; i++) { | ||
if (node === child) | ||
return [children[i - 1], children[i + 1]] | ||
return [children[i - 1], children[i + 1]]; | ||
} | ||
if (node === children[0] && children[1]) | ||
return [children[1]] | ||
return [children[1]]; | ||
if (node === children[children.length - 1] && children[children.length - 2]) | ||
return [children[children.length - 2]] | ||
return [] | ||
return [children[children.length - 2]]; | ||
return []; | ||
} | ||
function hasAdjacentJsxExpressionContainers(node, children) { | ||
if (!children) | ||
return false | ||
const childrenExcludingWhitespaceLiteral = children.filter(child => !isWhiteSpaceLiteral(child)); | ||
return false; | ||
const childrenExcludingWhitespaceLiteral = children.filter((child) => !isWhiteSpaceLiteral(child)); | ||
const adjSiblings = getAdjacentSiblings(node, childrenExcludingWhitespaceLiteral); | ||
return adjSiblings.some(x => x.type && x.type === 'JSXExpressionContainer') | ||
return adjSiblings.some((x) => x.type && x.type === "JSXExpressionContainer"); | ||
} | ||
function hasAdjacentJsx(node, children) { | ||
if (!children) | ||
return false | ||
const childrenExcludingWhitespaceLiteral = children.filter(child => !isWhiteSpaceLiteral(child)); | ||
return false; | ||
const childrenExcludingWhitespaceLiteral = children.filter((child) => !isWhiteSpaceLiteral(child)); | ||
const adjSiblings = getAdjacentSiblings(node, childrenExcludingWhitespaceLiteral); | ||
return adjSiblings.some(x => x.type && arrayIncludes(['JSXExpressionContainer', 'JSXElement'], x.type)) | ||
return adjSiblings.some((x) => x.type && arrayIncludes(["JSXExpressionContainer", "JSXElement"], x.type)); | ||
} | ||
function shouldCheckForUnnecessaryCurly(node, config) { | ||
const parent = node.parent; | ||
// Bail out if the parent is a JSXAttribute & its contents aren't | ||
// StringLiteral or TemplateLiteral since e.g | ||
// <App prop1={<CustomEl />} prop2={<CustomEl>...</CustomEl>} /> | ||
if ( | ||
parent.type && parent.type === 'JSXAttribute' | ||
&& (node.expression && node.expression.type | ||
&& node.expression.type !== 'Literal' | ||
&& node.expression.type !== 'StringLiteral' | ||
&& node.expression.type !== 'TemplateLiteral') | ||
) | ||
return false | ||
// If there are adjacent `JsxExpressionContainer` then there is no need, | ||
// to check for unnecessary curly braces. | ||
if (parent.type && parent.type === "JSXAttribute" && (node.expression && node.expression.type && node.expression.type !== "Literal" && node.expression.type !== "StringLiteral" && node.expression.type !== "TemplateLiteral")) | ||
return false; | ||
if (utils.isJSX(parent) && hasAdjacentJsxExpressionContainers(node, parent.children)) | ||
return false | ||
return false; | ||
if (containsWhitespaceExpression(node) && hasAdjacentJsx(node, parent.children)) | ||
return false | ||
if ( | ||
parent.children | ||
&& parent.children.length === 1 | ||
&& containsWhitespaceExpression(node) | ||
) | ||
return false | ||
return areRuleConditionsSatisfied(parent, config, OPTION_NEVER) | ||
return false; | ||
if (parent.children && parent.children.length === 1 && containsWhitespaceExpression(node)) | ||
return false; | ||
return areRuleConditionsSatisfied(parent, config, OPTION_NEVER); | ||
} | ||
function shouldCheckForMissingCurly(node, config) { | ||
if (utils.isJSX(node)) | ||
return config.propElementValues !== OPTION_IGNORE | ||
if ( | ||
isLineBreak(node.raw) | ||
|| containsOnlyHtmlEntities(node.raw) | ||
) | ||
return false | ||
return config.propElementValues !== OPTION_IGNORE; | ||
if (isLineBreak(node.raw) || containsOnlyHtmlEntities(node.raw)) | ||
return false; | ||
const parent = node.parent; | ||
if ( | ||
parent.children | ||
&& parent.children.length === 1 | ||
&& containsWhitespaceExpression(parent.children[0]) | ||
) | ||
return false | ||
return areRuleConditionsSatisfied(parent, config, OPTION_ALWAYS) | ||
if (parent.children && parent.children.length === 1 && containsWhitespaceExpression(parent.children[0])) | ||
return false; | ||
return areRuleConditionsSatisfied(parent, config, OPTION_ALWAYS); | ||
} | ||
// -------------------------------------------------------------------------- | ||
// Public | ||
// -------------------------------------------------------------------------- | ||
return { | ||
'JSXAttribute > JSXExpressionContainer > JSXElement': function (node) { | ||
"JSXAttribute > JSXExpressionContainer > JSXElement": function(node) { | ||
if (userConfig.propElementValues === OPTION_NEVER) | ||
reportUnnecessaryCurly(node.parent); | ||
}, | ||
JSXExpressionContainer(node) { | ||
@@ -401,11 +238,10 @@ if (shouldCheckForUnnecessaryCurly(node, userConfig)) | ||
}, | ||
'JSXAttribute > JSXElement, Literal, JSXText': function (node) { | ||
"JSXAttribute > JSXElement, Literal, JSXText": function(node) { | ||
if (shouldCheckForMissingCurly(node, userConfig)) | ||
reportMissingCurly(node); | ||
}, | ||
} | ||
}, | ||
} | ||
}; | ||
} | ||
}; | ||
exports.jsxCurlyBracePresence = jsxCurlyBracePresence; |
@@ -5,54 +5,36 @@ 'use strict'; | ||
/** | ||
* @fileoverview enforce consistent line breaks inside jsx curly | ||
*/ | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
function getNormalizedOption(context) { | ||
const rawOption = context.options[0] || 'consistent'; | ||
if (rawOption === 'consistent') { | ||
const rawOption = context.options[0] || "consistent"; | ||
if (rawOption === "consistent") { | ||
return { | ||
multiline: 'consistent', | ||
singleline: 'consistent', | ||
} | ||
multiline: "consistent", | ||
singleline: "consistent" | ||
}; | ||
} | ||
if (rawOption === 'never') { | ||
if (rawOption === "never") { | ||
return { | ||
multiline: 'forbid', | ||
singleline: 'forbid', | ||
} | ||
multiline: "forbid", | ||
singleline: "forbid" | ||
}; | ||
} | ||
return { | ||
multiline: rawOption.multiline || 'consistent', | ||
singleline: rawOption.singleline || 'consistent', | ||
} | ||
multiline: rawOption.multiline || "consistent", | ||
singleline: rawOption.singleline || "consistent" | ||
}; | ||
} | ||
const messages = { | ||
expectedBefore: 'Expected newline before \'}\'.', | ||
expectedAfter: 'Expected newline after \'{\'.', | ||
unexpectedBefore: 'Unexpected newline before \'}\'.', | ||
unexpectedAfter: 'Unexpected newline after \'{\'.', | ||
expectedBefore: "Expected newline before '}'.", | ||
expectedAfter: "Expected newline after '{'.", | ||
unexpectedBefore: "Unexpected newline before '}'.", | ||
unexpectedAfter: "Unexpected newline after '{'." | ||
}; | ||
var jsxCurlyNewline = { | ||
meta: { | ||
type: 'layout', | ||
type: "layout", | ||
docs: { | ||
description: 'Enforce consistent linebreaks in curly braces in JSX attributes and expressions', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('jsx-curly-newline'), | ||
description: "Enforce consistent linebreaks in curly braces in JSX attributes and expressions", | ||
category: "Stylistic Issues", | ||
url: utils.docsUrl("jsx-curly-newline") | ||
}, | ||
fixable: 'whitespace', | ||
fixable: "whitespace", | ||
schema: [ | ||
@@ -62,60 +44,35 @@ { | ||
{ | ||
enum: ['consistent', 'never'], | ||
enum: ["consistent", "never"] | ||
}, | ||
{ | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
singleline: { enum: ['consistent', 'require', 'forbid'] }, | ||
multiline: { enum: ['consistent', 'require', 'forbid'] }, | ||
singleline: { enum: ["consistent", "require", "forbid"] }, | ||
multiline: { enum: ["consistent", "require", "forbid"] } | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
}, | ||
additionalProperties: false | ||
} | ||
] | ||
} | ||
], | ||
messages, | ||
messages | ||
}, | ||
create(context) { | ||
const sourceCode = context.getSourceCode(); | ||
const option = getNormalizedOption(context); | ||
// ---------------------------------------------------------------------- | ||
// Helpers | ||
// ---------------------------------------------------------------------- | ||
/** | ||
* Determines whether two adjacent tokens are on the same line. | ||
* @param {object} left - The left token object. | ||
* @param {object} right - The right token object. | ||
* @returns {boolean} Whether or not the tokens are on the same line. | ||
*/ | ||
function isTokenOnSameLine(left, right) { | ||
return left.loc.end.line === right.loc.start.line | ||
return left.loc.end.line === right.loc.start.line; | ||
} | ||
/** | ||
* Determines whether there should be newlines inside curlys | ||
* @param {ASTNode} expression The expression contained in the curlys | ||
* @param {boolean} hasLeftNewline `true` if the left curly has a newline in the current code. | ||
* @returns {boolean} `true` if there should be newlines inside the function curlys | ||
*/ | ||
function shouldHaveNewlines(expression, hasLeftNewline) { | ||
const isMultiline = expression.loc.start.line !== expression.loc.end.line; | ||
switch (isMultiline ? option.multiline : option.singleline) { | ||
case 'forbid': return false | ||
case 'require': return true | ||
case 'consistent': | ||
default: return hasLeftNewline | ||
case "forbid": | ||
return false; | ||
case "require": | ||
return true; | ||
case "consistent": | ||
default: | ||
return hasLeftNewline; | ||
} | ||
} | ||
/** | ||
* Validates curlys | ||
* @param {object} curlys An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token | ||
* @param {ASTNode} expression The expression inside the curly | ||
* @returns {void} | ||
*/ | ||
function validateCurlys(curlys, expression) { | ||
@@ -129,51 +86,32 @@ const leftCurly = curlys.leftCurly; | ||
const needsNewlines = shouldHaveNewlines(expression, hasLeftNewline); | ||
if (hasLeftNewline && !needsNewlines) { | ||
utils.report(context, messages.unexpectedAfter, 'unexpectedAfter', { | ||
utils.report(context, messages.unexpectedAfter, "unexpectedAfter", { | ||
node: leftCurly, | ||
fix(fixer) { | ||
return sourceCode | ||
.getText() | ||
.slice(leftCurly.range[1], tokenAfterLeftCurly.range[0]) | ||
.trim() | ||
? null // If there is a comment between the { and the first element, don't do a fix. | ||
: fixer.removeRange([leftCurly.range[1], tokenAfterLeftCurly.range[0]]) | ||
}, | ||
return sourceCode.getText().slice(leftCurly.range[1], tokenAfterLeftCurly.range[0]).trim() ? null : fixer.removeRange([leftCurly.range[1], tokenAfterLeftCurly.range[0]]); | ||
} | ||
}); | ||
} | ||
else if (!hasLeftNewline && needsNewlines) { | ||
utils.report(context, messages.expectedAfter, 'expectedAfter', { | ||
} else if (!hasLeftNewline && needsNewlines) { | ||
utils.report(context, messages.expectedAfter, "expectedAfter", { | ||
node: leftCurly, | ||
fix: fixer => fixer.insertTextAfter(leftCurly, '\n'), | ||
fix: (fixer) => fixer.insertTextAfter(leftCurly, "\n") | ||
}); | ||
} | ||
if (hasRightNewline && !needsNewlines) { | ||
utils.report(context, messages.unexpectedBefore, 'unexpectedBefore', { | ||
utils.report(context, messages.unexpectedBefore, "unexpectedBefore", { | ||
node: rightCurly, | ||
fix(fixer) { | ||
return sourceCode | ||
.getText() | ||
.slice(tokenBeforeRightCurly.range[1], rightCurly.range[0]) | ||
.trim() | ||
? null // If there is a comment between the last element and the }, don't do a fix. | ||
: fixer.removeRange([ | ||
tokenBeforeRightCurly.range[1], | ||
rightCurly.range[0], | ||
]) | ||
}, | ||
return sourceCode.getText().slice(tokenBeforeRightCurly.range[1], rightCurly.range[0]).trim() ? null : fixer.removeRange([ | ||
tokenBeforeRightCurly.range[1], | ||
rightCurly.range[0] | ||
]); | ||
} | ||
}); | ||
} | ||
else if (!hasRightNewline && needsNewlines) { | ||
utils.report(context, messages.expectedBefore, 'expectedBefore', { | ||
} else if (!hasRightNewline && needsNewlines) { | ||
utils.report(context, messages.expectedBefore, "expectedBefore", { | ||
node: rightCurly, | ||
fix: fixer => fixer.insertTextBefore(rightCurly, '\n'), | ||
fix: (fixer) => fixer.insertTextBefore(rightCurly, "\n") | ||
}); | ||
} | ||
} | ||
// ---------------------------------------------------------------------- | ||
// Public | ||
// ---------------------------------------------------------------------- | ||
return { | ||
@@ -183,10 +121,10 @@ JSXExpressionContainer(node) { | ||
leftCurly: sourceCode.getFirstToken(node), | ||
rightCurly: sourceCode.getLastToken(node), | ||
rightCurly: sourceCode.getLastToken(node) | ||
}; | ||
validateCurlys(curlyTokens, node.expression); | ||
}, | ||
} | ||
}, | ||
} | ||
}; | ||
} | ||
}; | ||
exports.jsxCurlyNewline = jsxCurlyNewline; |
@@ -5,115 +5,92 @@ 'use strict'; | ||
/** | ||
* @fileoverview Enforce or disallow spaces inside of curly braces in JSX attributes. | ||
* @author Jamund Ferguson | ||
* @author Brandyn Bennett | ||
* @author Michael Ficarra | ||
* @author Vignesh Anand | ||
* @author Jamund Ferguson | ||
* @author Yannick Croissant | ||
* @author Erik Wendel | ||
*/ | ||
const has = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
const SPACING = { | ||
always: 'always', | ||
never: 'never', | ||
always: "always", | ||
never: "never" | ||
}; | ||
const SPACING_VALUES = [SPACING.always, SPACING.never]; | ||
const messages = { | ||
noNewlineAfter: 'There should be no newline after \'{{token}}\'', | ||
noNewlineBefore: 'There should be no newline before \'{{token}}\'', | ||
noSpaceAfter: 'There should be no space after \'{{token}}\'', | ||
noSpaceBefore: 'There should be no space before \'{{token}}\'', | ||
spaceNeededAfter: 'A space is required after \'{{token}}\'', | ||
spaceNeededBefore: 'A space is required before \'{{token}}\'', | ||
noNewlineAfter: "There should be no newline after '{{token}}'", | ||
noNewlineBefore: "There should be no newline before '{{token}}'", | ||
noSpaceAfter: "There should be no space after '{{token}}'", | ||
noSpaceBefore: "There should be no space before '{{token}}'", | ||
spaceNeededAfter: "A space is required after '{{token}}'", | ||
spaceNeededBefore: "A space is required before '{{token}}'" | ||
}; | ||
var jsxCurlySpacing = { | ||
meta: { | ||
docs: { | ||
description: 'Enforce or disallow spaces inside of curly braces in JSX attributes and expressions', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('jsx-curly-spacing'), | ||
description: "Enforce or disallow spaces inside of curly braces in JSX attributes and expressions", | ||
category: "Stylistic Issues", | ||
url: utils.docsUrl("jsx-curly-spacing") | ||
}, | ||
fixable: 'code', | ||
fixable: "code", | ||
messages, | ||
schema: { | ||
definitions: { | ||
basicConfig: { | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
when: { | ||
enum: SPACING_VALUES, | ||
enum: SPACING_VALUES | ||
}, | ||
allowMultiline: { | ||
type: 'boolean', | ||
type: "boolean" | ||
}, | ||
spacing: { | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
objectLiterals: { | ||
enum: SPACING_VALUES, | ||
}, | ||
}, | ||
}, | ||
}, | ||
enum: SPACING_VALUES | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
basicConfigOrBoolean: { | ||
anyOf: [{ | ||
$ref: '#/definitions/basicConfig', | ||
$ref: "#/definitions/basicConfig" | ||
}, { | ||
type: 'boolean', | ||
}], | ||
}, | ||
type: "boolean" | ||
}] | ||
} | ||
}, | ||
type: 'array', | ||
type: "array", | ||
items: [{ | ||
anyOf: [{ | ||
allOf: [{ | ||
$ref: '#/definitions/basicConfig', | ||
$ref: "#/definitions/basicConfig" | ||
}, { | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
attributes: { | ||
$ref: '#/definitions/basicConfigOrBoolean', | ||
$ref: "#/definitions/basicConfigOrBoolean" | ||
}, | ||
children: { | ||
$ref: '#/definitions/basicConfigOrBoolean', | ||
}, | ||
}, | ||
}], | ||
$ref: "#/definitions/basicConfigOrBoolean" | ||
} | ||
} | ||
}] | ||
}, { | ||
enum: SPACING_VALUES, | ||
}], | ||
enum: SPACING_VALUES | ||
}] | ||
}, { | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
allowMultiline: { | ||
type: 'boolean', | ||
type: "boolean" | ||
}, | ||
spacing: { | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
objectLiterals: { | ||
enum: SPACING_VALUES, | ||
}, | ||
}, | ||
}, | ||
enum: SPACING_VALUES | ||
} | ||
} | ||
} | ||
}, | ||
additionalProperties: false, | ||
}], | ||
}, | ||
additionalProperties: false | ||
}] | ||
} | ||
}, | ||
create(context) { | ||
@@ -123,17 +100,14 @@ function normalizeConfig(configOrTrue, defaults, lastPass) { | ||
const when = config.when || defaults.when; | ||
const allowMultiline = has(config, 'allowMultiline') ? config.allowMultiline : defaults.allowMultiline; | ||
const allowMultiline = has(config, "allowMultiline") ? config.allowMultiline : defaults.allowMultiline; | ||
const spacing = config.spacing || {}; | ||
let objectLiteralSpaces = spacing.objectLiterals || defaults.objectLiteralSpaces; | ||
if (lastPass) { | ||
// On the final pass assign the values that should be derived from others if they are still undefined | ||
objectLiteralSpaces = objectLiteralSpaces || when; | ||
} | ||
return { | ||
when, | ||
allowMultiline, | ||
objectLiteralSpaces, | ||
} | ||
objectLiteralSpaces | ||
}; | ||
} | ||
const DEFAULT_WHEN = SPACING.never; | ||
@@ -143,109 +117,62 @@ const DEFAULT_ALLOW_MULTILINE = true; | ||
const DEFAULT_CHILDREN = false; | ||
let originalConfig = context.options[0] || {}; | ||
if (SPACING_VALUES.includes(originalConfig)) | ||
originalConfig = Object.assign({ when: context.options[0] }, context.options[1]); | ||
const defaultConfig = normalizeConfig(originalConfig, { | ||
when: DEFAULT_WHEN, | ||
allowMultiline: DEFAULT_ALLOW_MULTILINE, | ||
allowMultiline: DEFAULT_ALLOW_MULTILINE | ||
}); | ||
const attributes = has(originalConfig, 'attributes') ? originalConfig.attributes : DEFAULT_ATTRIBUTES; | ||
const attributes = has(originalConfig, "attributes") ? originalConfig.attributes : DEFAULT_ATTRIBUTES; | ||
const attributesConfig = attributes ? normalizeConfig(attributes, defaultConfig, true) : null; | ||
const children = has(originalConfig, 'children') ? originalConfig.children : DEFAULT_CHILDREN; | ||
const children = has(originalConfig, "children") ? originalConfig.children : DEFAULT_CHILDREN; | ||
const childrenConfig = children ? normalizeConfig(children, defaultConfig, true) : null; | ||
// -------------------------------------------------------------------------- | ||
// Helpers | ||
// -------------------------------------------------------------------------- | ||
/** | ||
* Determines whether two adjacent tokens have a newline between them. | ||
* @param {object} left - The left token object. | ||
* @param {object} right - The right token object. | ||
* @returns {boolean} Whether or not there is a newline between the tokens. | ||
*/ | ||
function isMultiline(left, right) { | ||
return left.loc.end.line !== right.loc.start.line | ||
return left.loc.end.line !== right.loc.start.line; | ||
} | ||
/** | ||
* Trims text of whitespace between two ranges | ||
* @param {Fixer} fixer - the eslint fixer object | ||
* @param {number} fromLoc - the start location | ||
* @param {number} toLoc - the end location | ||
* @param {string} mode - either 'start' or 'end' | ||
* @param {string=} spacing - a spacing value that will optionally add a space to the removed text | ||
* @returns {object | * | {range, text}} | ||
*/ | ||
function fixByTrimmingWhitespace(fixer, fromLoc, toLoc, mode, spacing) { | ||
let replacementText = context.getSourceCode().text.slice(fromLoc, toLoc); | ||
if (mode === 'start') | ||
replacementText = replacementText.replace(/^\s+/gm, ''); | ||
if (mode === "start") | ||
replacementText = replacementText.replace(/^\s+/gm, ""); | ||
else | ||
replacementText = replacementText.replace(/\s+$/gm, ''); | ||
replacementText = replacementText.replace(/\s+$/gm, ""); | ||
if (spacing === SPACING.always) { | ||
if (mode === 'start') | ||
replacementText += ' '; | ||
if (mode === "start") | ||
replacementText += " "; | ||
else | ||
replacementText = ` ${replacementText}`; | ||
} | ||
return fixer.replaceTextRange([fromLoc, toLoc], replacementText) | ||
return fixer.replaceTextRange([fromLoc, toLoc], replacementText); | ||
} | ||
/** | ||
* Reports that there shouldn't be a newline after the first token | ||
* @param {ASTNode} node - The node to report in the event of an error. | ||
* @param {Token} token - The token to use for the report. | ||
* @param {string} spacing | ||
* @returns {void} | ||
*/ | ||
function reportNoBeginningNewline(node, token, spacing) { | ||
utils.report(context, messages.noNewlineAfter, 'noNewlineAfter', { | ||
utils.report(context, messages.noNewlineAfter, "noNewlineAfter", { | ||
node, | ||
loc: token.loc.start, | ||
data: { | ||
token: token.value, | ||
token: token.value | ||
}, | ||
fix(fixer) { | ||
const nextToken = context.getSourceCode().getTokenAfter(token); | ||
return fixByTrimmingWhitespace(fixer, token.range[1], nextToken.range[0], 'start', spacing) | ||
}, | ||
return fixByTrimmingWhitespace(fixer, token.range[1], nextToken.range[0], "start", spacing); | ||
} | ||
}); | ||
} | ||
/** | ||
* Reports that there shouldn't be a newline before the last token | ||
* @param {ASTNode} node - The node to report in the event of an error. | ||
* @param {Token} token - The token to use for the report. | ||
* @param {string} spacing | ||
* @returns {void} | ||
*/ | ||
function reportNoEndingNewline(node, token, spacing) { | ||
utils.report(context, messages.noNewlineBefore, 'noNewlineBefore', { | ||
utils.report(context, messages.noNewlineBefore, "noNewlineBefore", { | ||
node, | ||
loc: token.loc.start, | ||
data: { | ||
token: token.value, | ||
token: token.value | ||
}, | ||
fix(fixer) { | ||
const previousToken = context.getSourceCode().getTokenBefore(token); | ||
return fixByTrimmingWhitespace(fixer, previousToken.range[1], token.range[0], 'end', spacing) | ||
}, | ||
return fixByTrimmingWhitespace(fixer, previousToken.range[1], token.range[0], "end", spacing); | ||
} | ||
}); | ||
} | ||
/** | ||
* Reports that there shouldn't be a space after the first token | ||
* @param {ASTNode} node - The node to report in the event of an error. | ||
* @param {Token} token - The token to use for the report. | ||
* @returns {void} | ||
*/ | ||
function reportNoBeginningSpace(node, token) { | ||
utils.report(context, messages.noSpaceAfter, 'noSpaceAfter', { | ||
utils.report(context, messages.noSpaceAfter, "noSpaceAfter", { | ||
node, | ||
loc: token.loc.start, | ||
data: { | ||
token: token.value, | ||
token: token.value | ||
}, | ||
@@ -256,34 +183,20 @@ fix(fixer) { | ||
let nextComment; | ||
// eslint >=4.x | ||
if (sourceCode.getCommentsAfter) { | ||
nextComment = sourceCode.getCommentsAfter(token); | ||
// eslint 3.x | ||
} | ||
else { | ||
} else { | ||
const potentialComment = sourceCode.getTokenAfter(token, { includeComments: true }); | ||
nextComment = nextToken === potentialComment ? [] : [potentialComment]; | ||
} | ||
// Take comments into consideration to narrow the fix range to what is actually affected. (See #1414) | ||
if (nextComment.length > 0) | ||
return fixByTrimmingWhitespace(fixer, token.range[1], Math.min(nextToken.range[0], nextComment[0].range[0]), 'start') | ||
return fixByTrimmingWhitespace(fixer, token.range[1], nextToken.range[0], 'start') | ||
}, | ||
return fixByTrimmingWhitespace(fixer, token.range[1], Math.min(nextToken.range[0], nextComment[0].range[0]), "start"); | ||
return fixByTrimmingWhitespace(fixer, token.range[1], nextToken.range[0], "start"); | ||
} | ||
}); | ||
} | ||
/** | ||
* Reports that there shouldn't be a space before the last token | ||
* @param {ASTNode} node - The node to report in the event of an error. | ||
* @param {Token} token - The token to use for the report. | ||
* @returns {void} | ||
*/ | ||
function reportNoEndingSpace(node, token) { | ||
utils.report(context, messages.noSpaceBefore, 'noSpaceBefore', { | ||
utils.report(context, messages.noSpaceBefore, "noSpaceBefore", { | ||
node, | ||
loc: token.loc.start, | ||
data: { | ||
token: token.value, | ||
token: token.value | ||
}, | ||
@@ -294,84 +207,54 @@ fix(fixer) { | ||
let previousComment; | ||
// eslint >=4.x | ||
if (sourceCode.getCommentsBefore) { | ||
previousComment = sourceCode.getCommentsBefore(token); | ||
// eslint 3.x | ||
} | ||
else { | ||
} else { | ||
const potentialComment = sourceCode.getTokenBefore(token, { includeComments: true }); | ||
previousComment = previousToken === potentialComment ? [] : [potentialComment]; | ||
} | ||
// Take comments into consideration to narrow the fix range to what is actually affected. (See #1414) | ||
if (previousComment.length > 0) | ||
return fixByTrimmingWhitespace(fixer, Math.max(previousToken.range[1], previousComment[0].range[1]), token.range[0], 'end') | ||
return fixByTrimmingWhitespace(fixer, previousToken.range[1], token.range[0], 'end') | ||
}, | ||
return fixByTrimmingWhitespace(fixer, Math.max(previousToken.range[1], previousComment[0].range[1]), token.range[0], "end"); | ||
return fixByTrimmingWhitespace(fixer, previousToken.range[1], token.range[0], "end"); | ||
} | ||
}); | ||
} | ||
/** | ||
* Reports that there should be a space after the first token | ||
* @param {ASTNode} node - The node to report in the event of an error. | ||
* @param {Token} token - The token to use for the report. | ||
* @returns {void} | ||
*/ | ||
function reportRequiredBeginningSpace(node, token) { | ||
utils.report(context, messages.spaceNeededAfter, 'spaceNeededAfter', { | ||
utils.report(context, messages.spaceNeededAfter, "spaceNeededAfter", { | ||
node, | ||
loc: token.loc.start, | ||
data: { | ||
token: token.value, | ||
token: token.value | ||
}, | ||
fix(fixer) { | ||
return fixer.insertTextAfter(token, ' ') | ||
}, | ||
return fixer.insertTextAfter(token, " "); | ||
} | ||
}); | ||
} | ||
/** | ||
* Reports that there should be a space before the last token | ||
* @param {ASTNode} node - The node to report in the event of an error. | ||
* @param {Token} token - The token to use for the report. | ||
* @returns {void} | ||
*/ | ||
function reportRequiredEndingSpace(node, token) { | ||
utils.report(context, messages.spaceNeededBefore, 'spaceNeededBefore', { | ||
utils.report(context, messages.spaceNeededBefore, "spaceNeededBefore", { | ||
node, | ||
loc: token.loc.start, | ||
data: { | ||
token: token.value, | ||
token: token.value | ||
}, | ||
fix(fixer) { | ||
return fixer.insertTextBefore(token, ' ') | ||
}, | ||
return fixer.insertTextBefore(token, " "); | ||
} | ||
}); | ||
} | ||
/** | ||
* Determines if spacing in curly braces is valid. | ||
* @param {ASTNode} node The AST node to check. | ||
* @returns {void} | ||
*/ | ||
function validateBraceSpacing(node) { | ||
let config; | ||
switch (node.parent.type) { | ||
case 'JSXAttribute': | ||
case 'JSXOpeningElement': | ||
case "JSXAttribute": | ||
case "JSXOpeningElement": | ||
config = attributesConfig; | ||
break | ||
case 'JSXElement': | ||
case 'JSXFragment': | ||
break; | ||
case "JSXElement": | ||
case "JSXFragment": | ||
config = childrenConfig; | ||
break | ||
break; | ||
default: | ||
return | ||
return; | ||
} | ||
if (config === null) | ||
return | ||
return; | ||
const sourceCode = context.getSourceCode(); | ||
@@ -382,3 +265,2 @@ const first = sourceCode.getFirstToken(node); | ||
let penultimate = sourceCode.getTokenBefore(last, { includeComments: true }); | ||
if (!second) { | ||
@@ -394,3 +276,2 @@ second = sourceCode.getTokenAfter(first); | ||
} | ||
const isObjectLiteral = first.value === second.value; | ||
@@ -403,3 +284,2 @@ const spacing = isObjectLiteral ? config.objectLiteralSpaces : config.when; | ||
reportNoBeginningNewline(node, first, spacing); | ||
if (!sourceCode.isSpaceBetweenTokens(penultimate, last)) | ||
@@ -409,9 +289,7 @@ reportRequiredEndingSpace(node, last); | ||
reportNoEndingNewline(node, last, spacing); | ||
} | ||
else if (spacing === SPACING.never) { | ||
} else if (spacing === SPACING.never) { | ||
if (isMultiline(first, second)) { | ||
if (!config.allowMultiline) | ||
reportNoBeginningNewline(node, first, spacing); | ||
} | ||
else if (sourceCode.isSpaceBetweenTokens(first, second)) { | ||
} else if (sourceCode.isSpaceBetweenTokens(first, second)) { | ||
reportNoBeginningSpace(node, first); | ||
@@ -422,4 +300,3 @@ } | ||
reportNoEndingNewline(node, last, spacing); | ||
} | ||
else if (sourceCode.isSpaceBetweenTokens(penultimate, last)) { | ||
} else if (sourceCode.isSpaceBetweenTokens(penultimate, last)) { | ||
reportNoEndingSpace(node, last); | ||
@@ -429,14 +306,9 @@ } | ||
} | ||
// -------------------------------------------------------------------------- | ||
// Public | ||
// -------------------------------------------------------------------------- | ||
return { | ||
JSXExpressionContainer: validateBraceSpacing, | ||
JSXSpreadAttribute: validateBraceSpacing, | ||
} | ||
}, | ||
JSXSpreadAttribute: validateBraceSpacing | ||
}; | ||
} | ||
}; | ||
exports.jsxCurlySpacing = jsxCurlySpacing; |
@@ -5,52 +5,26 @@ 'use strict'; | ||
/** | ||
* @fileoverview Disallow or enforce spaces around equal signs in JSX attributes. | ||
* @author ryym | ||
*/ | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
const messages = { | ||
noSpaceBefore: 'There should be no space before \'=\'', | ||
noSpaceAfter: 'There should be no space after \'=\'', | ||
needSpaceBefore: 'A space is required before \'=\'', | ||
needSpaceAfter: 'A space is required after \'=\'', | ||
noSpaceBefore: "There should be no space before '='", | ||
noSpaceAfter: "There should be no space after '='", | ||
needSpaceBefore: "A space is required before '='", | ||
needSpaceAfter: "A space is required after '='" | ||
}; | ||
var jsxEqualsSpacing = { | ||
meta: { | ||
docs: { | ||
description: 'Enforce or disallow spaces around equal signs in JSX attributes', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('jsx-equals-spacing'), | ||
description: "Enforce or disallow spaces around equal signs in JSX attributes", | ||
category: "Stylistic Issues", | ||
url: utils.docsUrl("jsx-equals-spacing") | ||
}, | ||
fixable: 'code', | ||
fixable: "code", | ||
messages, | ||
schema: [{ | ||
enum: ['always', 'never'], | ||
}], | ||
enum: ["always", "never"] | ||
}] | ||
}, | ||
create(context) { | ||
const config = context.options[0] || 'never'; | ||
/** | ||
* Determines a given attribute node has an equal sign. | ||
* @param {ASTNode} attrNode - The attribute node. | ||
* @returns {boolean} Whether or not the attriute node has an equal sign. | ||
*/ | ||
const config = context.options[0] || "never"; | ||
function hasEqual(attrNode) { | ||
return attrNode.type !== 'JSXSpreadAttribute' && attrNode.value !== null | ||
return attrNode.type !== "JSXSpreadAttribute" && attrNode.value !== null; | ||
} | ||
// -------------------------------------------------------------------------- | ||
// Public | ||
// -------------------------------------------------------------------------- | ||
return { | ||
@@ -60,4 +34,3 @@ JSXOpeningElement(node) { | ||
if (!hasEqual(attrNode)) | ||
return | ||
return; | ||
const sourceCode = context.getSourceCode(); | ||
@@ -67,40 +40,38 @@ const equalToken = sourceCode.getTokenAfter(attrNode.name); | ||
const spacedAfter = sourceCode.isSpaceBetweenTokens(equalToken, attrNode.value); | ||
if (config === 'never') { | ||
if (config === "never") { | ||
if (spacedBefore) { | ||
utils.report(context, messages.noSpaceBefore, 'noSpaceBefore', { | ||
utils.report(context, messages.noSpaceBefore, "noSpaceBefore", { | ||
node: attrNode, | ||
loc: equalToken.loc.start, | ||
fix(fixer) { | ||
return fixer.removeRange([attrNode.name.range[1], equalToken.range[0]]) | ||
}, | ||
return fixer.removeRange([attrNode.name.range[1], equalToken.range[0]]); | ||
} | ||
}); | ||
} | ||
if (spacedAfter) { | ||
utils.report(context, messages.noSpaceAfter, 'noSpaceAfter', { | ||
utils.report(context, messages.noSpaceAfter, "noSpaceAfter", { | ||
node: attrNode, | ||
loc: equalToken.loc.start, | ||
fix(fixer) { | ||
return fixer.removeRange([equalToken.range[1], attrNode.value.range[0]]) | ||
}, | ||
return fixer.removeRange([equalToken.range[1], attrNode.value.range[0]]); | ||
} | ||
}); | ||
} | ||
} | ||
else if (config === 'always') { | ||
} else if (config === "always") { | ||
if (!spacedBefore) { | ||
utils.report(context, messages.needSpaceBefore, 'needSpaceBefore', { | ||
utils.report(context, messages.needSpaceBefore, "needSpaceBefore", { | ||
node: attrNode, | ||
loc: equalToken.loc.start, | ||
fix(fixer) { | ||
return fixer.insertTextBefore(equalToken, ' ') | ||
}, | ||
return fixer.insertTextBefore(equalToken, " "); | ||
} | ||
}); | ||
} | ||
if (!spacedAfter) { | ||
utils.report(context, messages.needSpaceAfter, 'needSpaceAfter', { | ||
utils.report(context, messages.needSpaceAfter, "needSpaceAfter", { | ||
node: attrNode, | ||
loc: equalToken.loc.start, | ||
fix(fixer) { | ||
return fixer.insertTextAfter(equalToken, ' ') | ||
}, | ||
return fixer.insertTextAfter(equalToken, " "); | ||
} | ||
}); | ||
@@ -110,7 +81,7 @@ } | ||
}); | ||
}, | ||
} | ||
}, | ||
} | ||
}; | ||
} | ||
}; | ||
exports.jsxEqualsSpacing = jsxEqualsSpacing; |
@@ -5,80 +5,54 @@ 'use strict'; | ||
/** | ||
* @fileoverview Ensure proper position of the first property in JSX | ||
* @author Joachim Seminck | ||
*/ | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
const messages = { | ||
propOnNewLine: 'Property should be placed on a new line', | ||
propOnSameLine: 'Property should be placed on the same line as the component declaration', | ||
propOnNewLine: "Property should be placed on a new line", | ||
propOnSameLine: "Property should be placed on the same line as the component declaration" | ||
}; | ||
var jsxFirstPropNewLine = { | ||
meta: { | ||
docs: { | ||
description: 'Enforce proper position of the first property in JSX', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('jsx-first-prop-new-line'), | ||
description: "Enforce proper position of the first property in JSX", | ||
category: "Stylistic Issues", | ||
url: utils.docsUrl("jsx-first-prop-new-line") | ||
}, | ||
fixable: 'code', | ||
fixable: "code", | ||
messages, | ||
schema: [{ | ||
enum: ['always', 'never', 'multiline', 'multiline-multiprop', 'multiprop'], | ||
}], | ||
enum: ["always", "never", "multiline", "multiline-multiprop", "multiprop"] | ||
}] | ||
}, | ||
create(context) { | ||
const configuration = context.options[0] || 'multiline-multiprop'; | ||
const configuration = context.options[0] || "multiline-multiprop"; | ||
function isMultilineJSX(jsxNode) { | ||
return jsxNode.loc.start.line < jsxNode.loc.end.line | ||
return jsxNode.loc.start.line < jsxNode.loc.end.line; | ||
} | ||
return { | ||
JSXOpeningElement(node) { | ||
if ( | ||
(configuration === 'multiline' && isMultilineJSX(node)) | ||
|| (configuration === 'multiline-multiprop' && isMultilineJSX(node) && node.attributes.length > 1) | ||
|| (configuration === 'multiprop' && node.attributes.length > 1) | ||
|| (configuration === 'always') | ||
) { | ||
if (configuration === "multiline" && isMultilineJSX(node) || configuration === "multiline-multiprop" && isMultilineJSX(node) && node.attributes.length > 1 || configuration === "multiprop" && node.attributes.length > 1 || configuration === "always") { | ||
node.attributes.some((decl) => { | ||
if (decl.loc.start.line === node.loc.start.line) { | ||
utils.report(context, messages.propOnNewLine, 'propOnNewLine', { | ||
utils.report(context, messages.propOnNewLine, "propOnNewLine", { | ||
node: decl, | ||
fix(fixer) { | ||
return fixer.replaceTextRange([(node.typeParameters || node.name).range[1], decl.range[0]], '\n') | ||
}, | ||
return fixer.replaceTextRange([(node.typeParameters || node.name).range[1], decl.range[0]], "\n"); | ||
} | ||
}); | ||
} | ||
return true | ||
return true; | ||
}); | ||
} | ||
else if ( | ||
(configuration === 'never' && node.attributes.length > 0) | ||
|| (configuration === 'multiprop' && isMultilineJSX(node) && node.attributes.length <= 1) | ||
) { | ||
} else if (configuration === "never" && node.attributes.length > 0 || configuration === "multiprop" && isMultilineJSX(node) && node.attributes.length <= 1) { | ||
const firstNode = node.attributes[0]; | ||
if (node.loc.start.line < firstNode.loc.start.line) { | ||
utils.report(context, messages.propOnSameLine, 'propOnSameLine', { | ||
utils.report(context, messages.propOnSameLine, "propOnSameLine", { | ||
node: firstNode, | ||
fix(fixer) { | ||
return fixer.replaceTextRange([node.name.range[1], firstNode.range[0]], ' ') | ||
}, | ||
return fixer.replaceTextRange([node.name.range[1], firstNode.range[0]], " "); | ||
} | ||
}); | ||
} | ||
} | ||
}, | ||
} | ||
}, | ||
} | ||
}; | ||
} | ||
}; | ||
exports.jsxFirstPropNewLine = jsxFirstPropNewLine; |
@@ -5,118 +5,61 @@ 'use strict'; | ||
/** | ||
* @fileoverview Validate props indentation in JSX | ||
* @author Yannick Croissant | ||
* | ||
* This rule has been ported and modified from eslint and nodeca. | ||
* @author Vitaly Puzrin | ||
* @author Gyandeep Singh | ||
* @copyright 2015 Vitaly Puzrin. All rights reserved. | ||
* @copyright 2015 Gyandeep Singh. All rights reserved. | ||
*/ | ||
/* | ||
Copyright (C) 2014 by Vitaly Puzrin | ||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the 'Software'), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
The above copyright notice and this permission notice shall be included in | ||
all copies or substantial portions of the Software. | ||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
THE SOFTWARE. | ||
*/ | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
const messages = { | ||
wrongIndent: 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.', | ||
wrongIndent: "Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}." | ||
}; | ||
var jsxIndentProps = { | ||
meta: { | ||
docs: { | ||
description: 'Enforce props indentation in JSX', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('jsx-indent-props'), | ||
description: "Enforce props indentation in JSX", | ||
category: "Stylistic Issues", | ||
url: utils.docsUrl("jsx-indent-props") | ||
}, | ||
fixable: 'code', | ||
fixable: "code", | ||
messages, | ||
schema: [{ | ||
anyOf: [{ | ||
enum: ['tab', 'first'], | ||
enum: ["tab", "first"] | ||
}, { | ||
type: 'integer', | ||
type: "integer" | ||
}, { | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
indentMode: { | ||
anyOf: [{ | ||
enum: ['tab', 'first'], | ||
enum: ["tab", "first"] | ||
}, { | ||
type: 'integer', | ||
}], | ||
type: "integer" | ||
}] | ||
}, | ||
ignoreTernaryOperator: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
}], | ||
}], | ||
type: "boolean" | ||
} | ||
} | ||
}] | ||
}] | ||
}, | ||
create(context) { | ||
const extraColumnStart = 0; | ||
let indentType = 'space'; | ||
/** @type {number|'first'} */ | ||
let indentType = "space"; | ||
let indentSize = 4; | ||
const line = { | ||
isUsingOperator: false, | ||
currentOperator: false, | ||
currentOperator: false | ||
}; | ||
let ignoreTernaryOperator = false; | ||
if (context.options.length) { | ||
const isConfigObject = typeof context.options[0] === 'object'; | ||
const indentMode = isConfigObject | ||
? context.options[0].indentMode | ||
: context.options[0]; | ||
if (indentMode === 'first') { | ||
indentSize = 'first'; | ||
indentType = 'space'; | ||
} | ||
else if (indentMode === 'tab') { | ||
const isConfigObject = typeof context.options[0] === "object"; | ||
const indentMode = isConfigObject ? context.options[0].indentMode : context.options[0]; | ||
if (indentMode === "first") { | ||
indentSize = "first"; | ||
indentType = "space"; | ||
} else if (indentMode === "tab") { | ||
indentSize = 1; | ||
indentType = 'tab'; | ||
} | ||
else if (typeof indentMode === 'number') { | ||
indentType = "tab"; | ||
} else if (typeof indentMode === "number") { | ||
indentSize = indentMode; | ||
indentType = 'space'; | ||
indentType = "space"; | ||
} | ||
if (isConfigObject && context.options[0].ignoreTernaryOperator) | ||
ignoreTernaryOperator = true; | ||
} | ||
/** | ||
* Reports a given indent violation and properly pluralizes the message | ||
* @param {ASTNode} node Node violating the indent rule | ||
* @param {number} needed Expected indentation character count | ||
* @param {number} gotten Indentation character count in the actual node/code | ||
*/ | ||
function report(node, needed, gotten) { | ||
@@ -126,35 +69,25 @@ const msgContext = { | ||
type: indentType, | ||
characters: needed === 1 ? 'character' : 'characters', | ||
gotten, | ||
characters: needed === 1 ? "character" : "characters", | ||
gotten | ||
}; | ||
utils.report(context, messages.wrongIndent, 'wrongIndent', { | ||
utils.report(context, messages.wrongIndent, "wrongIndent", { | ||
node, | ||
data: msgContext, | ||
fix(fixer) { | ||
return fixer.replaceTextRange([node.range[0] - node.loc.start.column, node.range[0]], Array(needed + 1).join(indentType === 'space' ? ' ' : '\t')) | ||
}, | ||
return fixer.replaceTextRange([node.range[0] - node.loc.start.column, node.range[0]], Array(needed + 1).join(indentType === "space" ? " " : " ")); | ||
} | ||
}); | ||
} | ||
/** | ||
* Get node indent | ||
* @param {ASTNode} node Node to examine | ||
* @return {number} Indent | ||
*/ | ||
function getNodeIndent(node) { | ||
let src = context.getSourceCode().getText(node, node.loc.start.column + extraColumnStart); | ||
const lines = src.split('\n'); | ||
const lines = src.split("\n"); | ||
src = lines[0]; | ||
let regExp; | ||
if (indentType === 'space') | ||
if (indentType === "space") | ||
regExp = /^[ ]+/; | ||
else | ||
regExp = /^[\t]+/; | ||
const indent = regExp.exec(src); | ||
const useOperator = /^([ ]|[\t])*[:]/.test(src) || /^([ ]|[\t])*[?]/.test(src); | ||
const useBracket = /[<]/.test(src); | ||
line.currentOperator = false; | ||
@@ -164,15 +97,7 @@ if (useOperator) { | ||
line.currentOperator = true; | ||
} | ||
else if (useBracket) { | ||
} else if (useBracket) { | ||
line.isUsingOperator = false; | ||
} | ||
return indent ? indent[0].length : 0 | ||
return indent ? indent[0].length : 0; | ||
} | ||
/** | ||
* Check indent for nodes list | ||
* @param {ASTNode[]} nodes list of node objects | ||
* @param {number} indent needed indent | ||
*/ | ||
function checkNodesIndent(nodes, indent) { | ||
@@ -182,30 +107,19 @@ let nestedIndent = indent; | ||
const nodeIndent = getNodeIndent(node); | ||
if ( | ||
line.isUsingOperator | ||
&& !line.currentOperator | ||
&& indentSize !== 'first' | ||
&& !ignoreTernaryOperator | ||
) { | ||
if (line.isUsingOperator && !line.currentOperator && indentSize !== "first" && !ignoreTernaryOperator) { | ||
nestedIndent += indentSize; | ||
line.isUsingOperator = false; | ||
} | ||
if ( | ||
node.type !== 'ArrayExpression' && node.type !== 'ObjectExpression' | ||
&& nodeIndent !== nestedIndent && utils.isNodeFirstInLine(context, node) | ||
) | ||
if (node.type !== "ArrayExpression" && node.type !== "ObjectExpression" && nodeIndent !== nestedIndent && utils.isNodeFirstInLine(context, node)) | ||
report(node, nestedIndent, nodeIndent); | ||
}); | ||
} | ||
return { | ||
JSXOpeningElement(node) { | ||
if (!node.attributes.length) | ||
return | ||
return; | ||
let propIndent; | ||
if (indentSize === 'first') { | ||
if (indentSize === "first") { | ||
const firstPropNode = node.attributes[0]; | ||
propIndent = firstPropNode.loc.start.column; | ||
} | ||
else { | ||
} else { | ||
const elementIndent = getNodeIndent(node); | ||
@@ -215,7 +129,7 @@ propIndent = elementIndent + indentSize; | ||
checkNodesIndent(node.attributes, propIndent); | ||
}, | ||
} | ||
}, | ||
} | ||
}; | ||
} | ||
}; | ||
exports.jsxIndentProps = jsxIndentProps; |
@@ -5,146 +5,83 @@ 'use strict'; | ||
/** | ||
* @fileoverview Validate JSX indentation | ||
* @author Yannick Croissant | ||
* | ||
* This rule has been ported and modified from eslint and nodeca. | ||
* @author Vitaly Puzrin | ||
* @author Gyandeep Singh | ||
* @copyright 2015 Vitaly Puzrin. All rights reserved. | ||
* @copyright 2015 Gyandeep Singh. All rights reserved. | ||
*/ | ||
/* | ||
Copyright (C) 2014 by Vitaly Puzrin | ||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the 'Software'), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
The above copyright notice and this permission notice shall be included in | ||
all copies or substantial portions of the Software. | ||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
THE SOFTWARE. | ||
*/ | ||
const matchAll = (s, v) => s.matchAll(v); | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
const messages = { | ||
wrongIndent: 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.', | ||
wrongIndent: "Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}." | ||
}; | ||
var jsxIndent = { | ||
meta: { | ||
docs: { | ||
description: 'Enforce JSX indentation', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('jsx-indent'), | ||
description: "Enforce JSX indentation", | ||
category: "Stylistic Issues", | ||
url: utils.docsUrl("jsx-indent") | ||
}, | ||
fixable: 'whitespace', | ||
fixable: "whitespace", | ||
messages, | ||
schema: [{ | ||
anyOf: [{ | ||
enum: ['tab'], | ||
enum: ["tab"] | ||
}, { | ||
type: 'integer', | ||
}], | ||
type: "integer" | ||
}] | ||
}, { | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
checkAttributes: { | ||
type: 'boolean', | ||
type: "boolean" | ||
}, | ||
indentLogicalExpressions: { | ||
type: 'boolean', | ||
}, | ||
type: "boolean" | ||
} | ||
}, | ||
additionalProperties: false, | ||
}], | ||
additionalProperties: false | ||
}] | ||
}, | ||
create(context) { | ||
const extraColumnStart = 0; | ||
let indentType = 'space'; | ||
let indentType = "space"; | ||
let indentSize = 4; | ||
if (context.options.length) { | ||
if (context.options[0] === 'tab') { | ||
if (context.options[0] === "tab") { | ||
indentSize = 1; | ||
indentType = 'tab'; | ||
} | ||
else if (typeof context.options[0] === 'number') { | ||
indentType = "tab"; | ||
} else if (typeof context.options[0] === "number") { | ||
indentSize = context.options[0]; | ||
indentType = 'space'; | ||
indentType = "space"; | ||
} | ||
} | ||
const indentChar = indentType === 'space' ? ' ' : '\t'; | ||
const indentChar = indentType === "space" ? " " : " "; | ||
const options = context.options[1] || {}; | ||
const checkAttributes = options.checkAttributes || false; | ||
const indentLogicalExpressions = options.indentLogicalExpressions || false; | ||
/** | ||
* Responsible for fixing the indentation issue fix | ||
* @param {ASTNode} node Node violating the indent rule | ||
* @param {number} needed Expected indentation character count | ||
* @returns {Function} function to be executed by the fixer | ||
* @private | ||
*/ | ||
function getFixerFunction(node, needed) { | ||
const indent = Array(needed + 1).join(indentChar); | ||
if (node.type === 'JSXText' || node.type === 'Literal') { | ||
if (node.type === "JSXText" || node.type === "Literal") { | ||
return function fix(fixer) { | ||
const regExp = /\n[\t ]*(\S)/g; | ||
const fixedText = node.raw.replace(regExp, (match, p1) => `\n${indent}${p1}`); | ||
return fixer.replaceText(node, fixedText) | ||
} | ||
const fixedText = node.raw.replace(regExp, (match, p1) => ` | ||
${indent}${p1}`); | ||
return fixer.replaceText(node, fixedText); | ||
}; | ||
} | ||
if (node.type === 'ReturnStatement') { | ||
if (node.type === "ReturnStatement") { | ||
const raw = context.getSourceCode().getText(node); | ||
const lines = raw.split('\n'); | ||
const lines = raw.split("\n"); | ||
if (lines.length > 1) { | ||
return function fix(fixer) { | ||
const lastLineStart = raw.lastIndexOf('\n'); | ||
const lastLine = raw.slice(lastLineStart).replace(/^\n[\t ]*(\S)/, (match, p1) => `\n${indent}${p1}`); | ||
const lastLineStart = raw.lastIndexOf("\n"); | ||
const lastLine = raw.slice(lastLineStart).replace(/^\n[\t ]*(\S)/, (match, p1) => ` | ||
${indent}${p1}`); | ||
return fixer.replaceTextRange( | ||
[node.range[0] + lastLineStart, node.range[1]], | ||
lastLine, | ||
) | ||
} | ||
lastLine | ||
); | ||
}; | ||
} | ||
} | ||
return function fix(fixer) { | ||
return fixer.replaceTextRange( | ||
[node.range[0] - node.loc.start.column, node.range[0]], | ||
indent, | ||
) | ||
} | ||
indent | ||
); | ||
}; | ||
} | ||
/** | ||
* Reports a given indent violation and properly pluralizes the message | ||
* @param {ASTNode} node Node violating the indent rule | ||
* @param {number} needed Expected indentation character count | ||
* @param {number} gotten Indentation character count in the actual node/code | ||
* @param {object} [loc] Error line and column location | ||
*/ | ||
function report(node, needed, gotten, loc) { | ||
@@ -154,23 +91,14 @@ const msgContext = { | ||
type: indentType, | ||
characters: needed === 1 ? 'character' : 'characters', | ||
gotten, | ||
characters: needed === 1 ? "character" : "characters", | ||
gotten | ||
}; | ||
utils.report(context, messages.wrongIndent, 'wrongIndent', Object.assign({ | ||
utils.report(context, messages.wrongIndent, "wrongIndent", Object.assign({ | ||
node, | ||
data: msgContext, | ||
fix: getFixerFunction(node, needed), | ||
fix: getFixerFunction(node, needed) | ||
}, loc && { loc })); | ||
} | ||
/** | ||
* Get node indent | ||
* @param {ASTNode} node Node to examine | ||
* @param {boolean} [byLastLine] get indent of node's last line | ||
* @param {boolean} [excludeCommas] skip comma on start of line | ||
* @return {number} Indent | ||
*/ | ||
function getNodeIndent(node, byLastLine, excludeCommas) { | ||
let src = context.getSourceCode().getText(node, node.loc.start.column + extraColumnStart); | ||
const lines = src.split('\n'); | ||
const lines = src.split("\n"); | ||
if (byLastLine) | ||
@@ -180,150 +108,45 @@ src = lines[lines.length - 1]; | ||
src = lines[0]; | ||
const skip = excludeCommas ? ',' : ''; | ||
const skip = excludeCommas ? "," : ""; | ||
let regExp; | ||
if (indentType === 'space') | ||
if (indentType === "space") | ||
regExp = new RegExp(`^[ ${skip}]+`); | ||
else | ||
regExp = new RegExp(`^[\t${skip}]+`); | ||
regExp = new RegExp(`^[ ${skip}]+`); | ||
const indent = regExp.exec(src); | ||
return indent ? indent[0].length : 0 | ||
return indent ? indent[0].length : 0; | ||
} | ||
/** | ||
* Check if the node is the right member of a logical expression | ||
* @param {ASTNode} node The node to check | ||
* @return {boolean} true if its the case, false if not | ||
*/ | ||
function isRightInLogicalExp(node) { | ||
return ( | ||
node.parent | ||
&& node.parent.parent | ||
&& node.parent.parent.type === 'LogicalExpression' | ||
&& node.parent.parent.right === node.parent | ||
&& !indentLogicalExpressions | ||
) | ||
return node.parent && node.parent.parent && node.parent.parent.type === "LogicalExpression" && node.parent.parent.right === node.parent && !indentLogicalExpressions; | ||
} | ||
/** | ||
* Check if the node is the alternate member of a conditional expression | ||
* @param {ASTNode} node The node to check | ||
* @return {boolean} true if its the case, false if not | ||
*/ | ||
function isAlternateInConditionalExp(node) { | ||
return ( | ||
node.parent | ||
&& node.parent.parent | ||
&& node.parent.parent.type === 'ConditionalExpression' | ||
&& node.parent.parent.alternate === node.parent | ||
&& context.getSourceCode().getTokenBefore(node).value !== '(' | ||
) | ||
return node.parent && node.parent.parent && node.parent.parent.type === "ConditionalExpression" && node.parent.parent.alternate === node.parent && context.getSourceCode().getTokenBefore(node).value !== "("; | ||
} | ||
/** | ||
* Check if the node is within a DoExpression block but not the first expression (which need to be indented) | ||
* @param {ASTNode} node The node to check | ||
* @return {boolean} true if its the case, false if not | ||
*/ | ||
function isSecondOrSubsequentExpWithinDoExp(node) { | ||
/* | ||
It returns true when node.parent.parent.parent.parent matches: | ||
DoExpression({ | ||
..., | ||
body: BlockStatement({ | ||
..., | ||
body: [ | ||
..., // 1-n times | ||
ExpressionStatement({ | ||
..., | ||
expression: JSXElement({ | ||
..., | ||
openingElement: JSXOpeningElement() // the node | ||
}) | ||
}), | ||
... // 0-n times | ||
] | ||
}) | ||
}) | ||
except: | ||
DoExpression({ | ||
..., | ||
body: BlockStatement({ | ||
..., | ||
body: [ | ||
ExpressionStatement({ | ||
..., | ||
expression: JSXElement({ | ||
..., | ||
openingElement: JSXOpeningElement() // the node | ||
}) | ||
}), | ||
... // 0-n times | ||
] | ||
}) | ||
}) | ||
*/ | ||
const isInExpStmt = ( | ||
node.parent | ||
&& node.parent.parent | ||
&& node.parent.parent.type === 'ExpressionStatement' | ||
); | ||
const isInExpStmt = node.parent && node.parent.parent && node.parent.parent.type === "ExpressionStatement"; | ||
if (!isInExpStmt) | ||
return false | ||
return false; | ||
const expStmt = node.parent.parent; | ||
const isInBlockStmtWithinDoExp = ( | ||
expStmt.parent | ||
&& expStmt.parent.type === 'BlockStatement' | ||
&& expStmt.parent.parent | ||
&& expStmt.parent.parent.type === 'DoExpression' | ||
); | ||
const isInBlockStmtWithinDoExp = expStmt.parent && expStmt.parent.type === "BlockStatement" && expStmt.parent.parent && expStmt.parent.parent.type === "DoExpression"; | ||
if (!isInBlockStmtWithinDoExp) | ||
return false | ||
return false; | ||
const blockStmt = expStmt.parent; | ||
const blockStmtFirstExp = blockStmt.body[0]; | ||
return !(blockStmtFirstExp === expStmt) | ||
return !(blockStmtFirstExp === expStmt); | ||
} | ||
/** | ||
* Check indent for nodes list | ||
* @param {ASTNode} node The node to check | ||
* @param {number} indent needed indent | ||
* @param {boolean} [excludeCommas] skip comma on start of line | ||
*/ | ||
function checkNodesIndent(node, indent, excludeCommas) { | ||
const nodeIndent = getNodeIndent(node, false, excludeCommas); | ||
const isCorrectRightInLogicalExp = isRightInLogicalExp(node) && (nodeIndent - indent) === indentSize; | ||
const isCorrectAlternateInCondExp = isAlternateInConditionalExp(node) && (nodeIndent - indent) === 0; | ||
if ( | ||
nodeIndent !== indent | ||
&& utils.isNodeFirstInLine(context, node) | ||
&& !isCorrectRightInLogicalExp | ||
&& !isCorrectAlternateInCondExp | ||
) | ||
const isCorrectRightInLogicalExp = isRightInLogicalExp(node) && nodeIndent - indent === indentSize; | ||
const isCorrectAlternateInCondExp = isAlternateInConditionalExp(node) && nodeIndent - indent === 0; | ||
if (nodeIndent !== indent && utils.isNodeFirstInLine(context, node) && !isCorrectRightInLogicalExp && !isCorrectAlternateInCondExp) | ||
report(node, indent, nodeIndent); | ||
} | ||
/** | ||
* Check indent for Literal Node or JSXText Node | ||
* @param {ASTNode} node The node to check | ||
* @param {number} indent needed indent | ||
*/ | ||
function checkLiteralNodeIndent(node, indent) { | ||
const value = node.value; | ||
const regExp = indentType === 'space' ? /\n( *)[\t ]*\S/g : /\n(\t*)[\t ]*\S/g; | ||
const regExp = indentType === "space" ? /\n( *)[\t ]*\S/g : /\n(\t*)[\t ]*\S/g; | ||
const nodeIndentsPerLine = Array.from( | ||
matchAll(String(value), regExp), | ||
match => (match[1] ? match[1].length : 0), | ||
(match) => match[1] ? match[1].length : 0 | ||
); | ||
const hasFirstInLineNode = nodeIndentsPerLine.length > 0; | ||
if ( | ||
hasFirstInLineNode | ||
&& !nodeIndentsPerLine.every(actualIndent => actualIndent === indent) | ||
) { | ||
if (hasFirstInLineNode && !nodeIndentsPerLine.every((actualIndent) => actualIndent === indent)) { | ||
nodeIndentsPerLine.forEach((nodeIndent) => { | ||
@@ -334,3 +157,2 @@ report(node, indent, nodeIndent); | ||
} | ||
function handleOpeningElement(node) { | ||
@@ -340,41 +162,28 @@ const sourceCode = context.getSourceCode(); | ||
if (!prevToken) | ||
return | ||
// Use the parent in a list or an array | ||
if (prevToken.type === 'JSXText' || ((prevToken.type === 'Punctuator') && prevToken.value === ',')) { | ||
return; | ||
if (prevToken.type === "JSXText" || prevToken.type === "Punctuator" && prevToken.value === ",") { | ||
prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]); | ||
prevToken = prevToken.type === 'Literal' || prevToken.type === 'JSXText' ? prevToken.parent : prevToken; | ||
// Use the first non-punctuator token in a conditional expression | ||
} | ||
else if (prevToken.type === 'Punctuator' && prevToken.value === ':') { | ||
prevToken = prevToken.type === "Literal" || prevToken.type === "JSXText" ? prevToken.parent : prevToken; | ||
} else if (prevToken.type === "Punctuator" && prevToken.value === ":") { | ||
do | ||
prevToken = sourceCode.getTokenBefore(prevToken); | ||
while (prevToken.type === 'Punctuator' && prevToken.value !== '/') | ||
while (prevToken.type === "Punctuator" && prevToken.value !== "/"); | ||
prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]); | ||
while (prevToken.parent && prevToken.parent.type !== 'ConditionalExpression') | ||
while (prevToken.parent && prevToken.parent.type !== "ConditionalExpression") | ||
prevToken = prevToken.parent; | ||
} | ||
prevToken = prevToken.type === 'JSXExpressionContainer' ? prevToken.expression : prevToken; | ||
prevToken = prevToken.type === "JSXExpressionContainer" ? prevToken.expression : prevToken; | ||
const parentElementIndent = getNodeIndent(prevToken); | ||
const indent = ( | ||
prevToken.loc.start.line === node.loc.start.line | ||
|| isRightInLogicalExp(node) | ||
|| isAlternateInConditionalExp(node) | ||
|| isSecondOrSubsequentExpWithinDoExp(node) | ||
) ? 0 : indentSize; | ||
const indent = prevToken.loc.start.line === node.loc.start.line || isRightInLogicalExp(node) || isAlternateInConditionalExp(node) || isSecondOrSubsequentExpWithinDoExp(node) ? 0 : indentSize; | ||
checkNodesIndent(node, parentElementIndent + indent); | ||
} | ||
function handleClosingElement(node) { | ||
if (!node.parent) | ||
return | ||
return; | ||
const peerElementIndent = getNodeIndent(node.parent.openingElement || node.parent.openingFragment); | ||
checkNodesIndent(node, peerElementIndent); | ||
} | ||
function handleAttribute(node) { | ||
if (!checkAttributes || (!node.value || node.value.type !== 'JSXExpressionContainer')) | ||
return | ||
if (!checkAttributes || (!node.value || node.value.type !== "JSXExpressionContainer")) | ||
return; | ||
const nameIndent = getNodeIndent(node.name); | ||
@@ -386,14 +195,10 @@ const lastToken = context.getSourceCode().getLastToken(node.value); | ||
} | ||
function handleLiteral(node) { | ||
if (!node.parent) | ||
return | ||
if (node.parent.type !== 'JSXElement' && node.parent.type !== 'JSXFragment') | ||
return | ||
return; | ||
if (node.parent.type !== "JSXElement" && node.parent.type !== "JSXFragment") | ||
return; | ||
const parentNodeIndent = getNodeIndent(node.parent); | ||
checkLiteralNodeIndent(node, parentNodeIndent + indentSize); | ||
} | ||
return { | ||
@@ -407,4 +212,3 @@ JSXOpeningElement: handleOpeningElement, | ||
if (!node.parent) | ||
return | ||
return; | ||
const parentNodeIndent = getNodeIndent(node.parent); | ||
@@ -415,30 +219,19 @@ checkNodesIndent(node, parentNodeIndent + indentSize); | ||
JSXText: handleLiteral, | ||
ReturnStatement(node) { | ||
if ( | ||
!node.parent | ||
|| !utils.isJSX(node.argument) | ||
) | ||
return | ||
if (!node.parent || !utils.isJSX(node.argument)) | ||
return; | ||
let fn = node.parent; | ||
while (fn && fn.type !== 'FunctionDeclaration' && fn.type !== 'FunctionExpression') | ||
while (fn && fn.type !== "FunctionDeclaration" && fn.type !== "FunctionExpression") | ||
fn = fn.parent; | ||
if ( | ||
!fn | ||
|| !utils.isReturningJSX(node, context, true) | ||
) | ||
return | ||
if (!fn || !utils.isReturningJSX(node, context, true)) | ||
return; | ||
const openingIndent = getNodeIndent(node); | ||
const closingIndent = getNodeIndent(node, true); | ||
if (openingIndent !== closingIndent) | ||
report(node, openingIndent, closingIndent); | ||
}, | ||
} | ||
}, | ||
} | ||
}; | ||
} | ||
}; | ||
exports.jsxIndent = jsxIndent; |
@@ -5,85 +5,64 @@ 'use strict'; | ||
/** | ||
* @fileoverview Limit maximum of props on a single line in JSX | ||
* @author Yannick Croissant | ||
*/ | ||
function getPropName(context, propNode) { | ||
if (propNode.type === 'JSXSpreadAttribute') | ||
return context.getSourceCode().getText(propNode.argument) | ||
return propNode.name.name | ||
if (propNode.type === "JSXSpreadAttribute") | ||
return context.getSourceCode().getText(propNode.argument); | ||
return propNode.name.name; | ||
} | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
const messages = { | ||
newLine: 'Prop `{{prop}}` must be placed on a new line', | ||
newLine: "Prop `{{prop}}` must be placed on a new line" | ||
}; | ||
var jsxMaxPropsPerLine = { | ||
meta: { | ||
docs: { | ||
description: 'Enforce maximum of props on a single line in JSX', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('jsx-max-props-per-line'), | ||
description: "Enforce maximum of props on a single line in JSX", | ||
category: "Stylistic Issues", | ||
url: utils.docsUrl("jsx-max-props-per-line") | ||
}, | ||
fixable: 'code', | ||
fixable: "code", | ||
messages, | ||
schema: [{ | ||
anyOf: [{ | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
maximum: { | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
single: { | ||
type: 'integer', | ||
minimum: 1, | ||
type: "integer", | ||
minimum: 1 | ||
}, | ||
multi: { | ||
type: 'integer', | ||
minimum: 1, | ||
}, | ||
}, | ||
}, | ||
type: "integer", | ||
minimum: 1 | ||
} | ||
} | ||
} | ||
}, | ||
additionalProperties: false, | ||
additionalProperties: false | ||
}, { | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
maximum: { | ||
type: 'number', | ||
minimum: 1, | ||
type: "number", | ||
minimum: 1 | ||
}, | ||
when: { | ||
type: 'string', | ||
enum: ['always', 'multiline'], | ||
}, | ||
type: "string", | ||
enum: ["always", "multiline"] | ||
} | ||
}, | ||
additionalProperties: false, | ||
}], | ||
}], | ||
additionalProperties: false | ||
}] | ||
}] | ||
}, | ||
create(context) { | ||
const configuration = context.options[0] || {}; | ||
const maximum = configuration.maximum || 1; | ||
const maxConfig = typeof maximum === 'number' | ||
? { | ||
single: configuration.when === 'multiline' ? Infinity : maximum, | ||
multi: maximum, | ||
} | ||
: { | ||
single: maximum.single || Infinity, | ||
multi: maximum.multi || Infinity, | ||
}; | ||
const maxConfig = typeof maximum === "number" ? { | ||
single: configuration.when === "multiline" ? Infinity : maximum, | ||
multi: maximum | ||
} : { | ||
single: maximum.single || Infinity, | ||
multi: maximum.multi || Infinity | ||
}; | ||
function generateFixFunction(line, max) { | ||
@@ -94,33 +73,24 @@ const sourceCode = context.getSourceCode(); | ||
const back = line[line.length - 1].range[1]; | ||
for (let i = 0; i < line.length; i += max) { | ||
const nodes = line.slice(i, i + max); | ||
output.push(nodes.reduce((prev, curr) => { | ||
if (prev === '') | ||
return sourceCode.getText(curr) | ||
return `${prev} ${sourceCode.getText(curr)}` | ||
}, '')); | ||
if (prev === "") | ||
return sourceCode.getText(curr); | ||
return `${prev} ${sourceCode.getText(curr)}`; | ||
}, "")); | ||
} | ||
const code = output.join('\n'); | ||
const code = output.join("\n"); | ||
return function fix(fixer) { | ||
return fixer.replaceTextRange([front, back], code) | ||
} | ||
return fixer.replaceTextRange([front, back], code); | ||
}; | ||
} | ||
return { | ||
JSXOpeningElement(node) { | ||
if (!node.attributes.length) | ||
return | ||
return; | ||
const isSingleLineTag = node.loc.start.line === node.loc.end.line; | ||
if ((isSingleLineTag ? maxConfig.single : maxConfig.multi) === Infinity) | ||
return | ||
return; | ||
const firstProp = node.attributes[0]; | ||
const linePartitionedProps = [[firstProp]]; | ||
node.attributes.reduce((last, decl) => { | ||
@@ -131,27 +101,22 @@ if (last.loc.end.line === decl.loc.start.line) | ||
linePartitionedProps.push([decl]); | ||
return decl | ||
return decl; | ||
}); | ||
linePartitionedProps.forEach((propsInLine) => { | ||
const maxPropsCountPerLine = isSingleLineTag && propsInLine[0].loc.start.line === node.loc.start.line | ||
? maxConfig.single | ||
: maxConfig.multi; | ||
const maxPropsCountPerLine = isSingleLineTag && propsInLine[0].loc.start.line === node.loc.start.line ? maxConfig.single : maxConfig.multi; | ||
if (propsInLine.length > maxPropsCountPerLine) { | ||
const name = getPropName(context, propsInLine[maxPropsCountPerLine]); | ||
utils.report(context, messages.newLine, 'newLine', { | ||
utils.report(context, messages.newLine, "newLine", { | ||
node: propsInLine[maxPropsCountPerLine], | ||
data: { | ||
prop: name, | ||
prop: name | ||
}, | ||
fix: generateFixFunction(propsInLine, maxPropsCountPerLine), | ||
fix: generateFixFunction(propsInLine, maxPropsCountPerLine) | ||
}); | ||
} | ||
}); | ||
}, | ||
} | ||
}, | ||
} | ||
}; | ||
} | ||
}; | ||
exports.jsxMaxPropsPerLine = jsxMaxPropsPerLine; |
@@ -5,46 +5,31 @@ 'use strict'; | ||
/** | ||
* @fileoverview Require or prevent a new line after jsx elements and expressions. | ||
* @author Johnny Zabala | ||
* @author Joseph Stiles | ||
*/ | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
const messages = { | ||
require: 'JSX element should start in a new line', | ||
prevent: 'JSX element should not start in a new line', | ||
allowMultilines: 'Multiline JSX elements should start in a new line', | ||
require: "JSX element should start in a new line", | ||
prevent: "JSX element should not start in a new line", | ||
allowMultilines: "Multiline JSX elements should start in a new line" | ||
}; | ||
function isMultilined(node) { | ||
return node && node.loc.start.line !== node.loc.end.line | ||
return node && node.loc.start.line !== node.loc.end.line; | ||
} | ||
var jsxNewline = { | ||
meta: { | ||
docs: { | ||
description: 'Require or prevent a new line after jsx elements and expressions.', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('jsx-newline'), | ||
description: "Require or prevent a new line after jsx elements and expressions.", | ||
category: "Stylistic Issues", | ||
url: utils.docsUrl("jsx-newline") | ||
}, | ||
fixable: 'code', | ||
fixable: "code", | ||
messages, | ||
schema: [ | ||
{ | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
prevent: { | ||
default: false, | ||
type: 'boolean', | ||
type: "boolean" | ||
}, | ||
allowMultilines: { | ||
default: false, | ||
type: 'boolean', | ||
}, | ||
type: "boolean" | ||
} | ||
}, | ||
@@ -55,5 +40,5 @@ additionalProperties: false, | ||
allowMultilines: { | ||
const: true, | ||
}, | ||
}, | ||
const: true | ||
} | ||
} | ||
}, | ||
@@ -63,64 +48,45 @@ then: { | ||
prevent: { | ||
const: true, | ||
}, | ||
const: true | ||
} | ||
}, | ||
required: [ | ||
'prevent', | ||
], | ||
}, | ||
}, | ||
], | ||
"prevent" | ||
] | ||
} | ||
} | ||
] | ||
}, | ||
create(context) { | ||
const jsxElementParents = new Set(); | ||
const jsxElementParents = /* @__PURE__ */ new Set(); | ||
const sourceCode = context.getSourceCode(); | ||
function isBlockCommentInCurlyBraces(element) { | ||
const elementRawValue = sourceCode.getText(element); | ||
return /^\s*{\/\*/.test(elementRawValue) | ||
return /^\s*{\/\*/.test(elementRawValue); | ||
} | ||
function isNonBlockComment(element) { | ||
return !isBlockCommentInCurlyBraces(element) && (element.type === 'JSXElement' || element.type === 'JSXExpressionContainer') | ||
return !isBlockCommentInCurlyBraces(element) && (element.type === "JSXElement" || element.type === "JSXExpressionContainer"); | ||
} | ||
return { | ||
'Program:exit': function () { | ||
"Program:exit": function() { | ||
jsxElementParents.forEach((parent) => { | ||
parent.children.forEach((element, index, elements) => { | ||
if (element.type === 'JSXElement' || element.type === 'JSXExpressionContainer') { | ||
if (element.type === "JSXElement" || element.type === "JSXExpressionContainer") { | ||
const configuration = context.options[0] || {}; | ||
const prevent = configuration.prevent || false; | ||
const allowMultilines = configuration.allowMultilines || false; | ||
const firstAdjacentSibling = elements[index + 1]; | ||
const secondAdjacentSibling = elements[index + 2]; | ||
const hasSibling = firstAdjacentSibling | ||
&& secondAdjacentSibling | ||
&& (firstAdjacentSibling.type === 'Literal' || firstAdjacentSibling.type === 'JSXText'); | ||
const hasSibling = firstAdjacentSibling && secondAdjacentSibling && (firstAdjacentSibling.type === "Literal" || firstAdjacentSibling.type === "JSXText"); | ||
if (!hasSibling) | ||
return | ||
// Check adjacent sibling has the proper amount of newlines | ||
return; | ||
const isWithoutNewLine = !/\n\s*\n/.test(firstAdjacentSibling.value); | ||
if (isBlockCommentInCurlyBraces(element)) | ||
return | ||
if ( | ||
allowMultilines | ||
&& ( | ||
isMultilined(element) | ||
|| isMultilined(elements.slice(index + 2).find(isNonBlockComment)) | ||
) | ||
) { | ||
return; | ||
if (allowMultilines && (isMultilined(element) || isMultilined(elements.slice(index + 2).find(isNonBlockComment)))) { | ||
if (!isWithoutNewLine) | ||
return | ||
const regex = /(\n)(?!.*\1)/g; | ||
const replacement = '\n\n'; | ||
const messageId = 'allowMultilines'; | ||
utils.report(context, messages[messageId], messageId, { | ||
return; | ||
const regex2 = /(\n)(?!.*\1)/g; | ||
const replacement2 = "\n\n"; | ||
const messageId2 = "allowMultilines"; | ||
utils.report(context, messages[messageId2], messageId2, { | ||
node: secondAdjacentSibling, | ||
@@ -130,26 +96,13 @@ fix(fixer) { | ||
firstAdjacentSibling, | ||
sourceCode.getText(firstAdjacentSibling) | ||
.replace(regex, replacement), | ||
) | ||
}, | ||
sourceCode.getText(firstAdjacentSibling).replace(regex2, replacement2) | ||
); | ||
} | ||
}); | ||
return | ||
return; | ||
} | ||
if (isWithoutNewLine === prevent) | ||
return | ||
const messageId = prevent | ||
? 'prevent' | ||
: 'require'; | ||
const regex = prevent | ||
? /(\n\n)(?!.*\1)/g | ||
: /(\n)(?!.*\1)/g; | ||
const replacement = prevent | ||
? '\n' | ||
: '\n\n'; | ||
return; | ||
const messageId = prevent ? "prevent" : "require"; | ||
const regex = prevent ? /(\n\n)(?!.*\1)/g : /(\n)(?!.*\1)/g; | ||
const replacement = prevent ? "\n" : "\n\n"; | ||
utils.report(context, messages[messageId], messageId, { | ||
@@ -161,6 +114,5 @@ node: secondAdjacentSibling, | ||
// double or remove the last newline | ||
sourceCode.getText(firstAdjacentSibling) | ||
.replace(regex, replacement), | ||
) | ||
}, | ||
sourceCode.getText(firstAdjacentSibling).replace(regex, replacement) | ||
); | ||
} | ||
}); | ||
@@ -171,9 +123,9 @@ } | ||
}, | ||
':matches(JSXElement, JSXFragment) > :matches(JSXElement, JSXExpressionContainer)': (node) => { | ||
":matches(JSXElement, JSXFragment) > :matches(JSXElement, JSXExpressionContainer)": (node) => { | ||
jsxElementParents.add(node.parent); | ||
}, | ||
} | ||
}, | ||
} | ||
}; | ||
} | ||
}; | ||
exports.jsxNewline = jsxNewline; |
@@ -5,63 +5,42 @@ 'use strict'; | ||
/** | ||
* @fileoverview Limit to one expression per line in JSX | ||
* @author Mark Ivan Allen <Vydia.com> | ||
*/ | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
const optionDefaults = { | ||
allow: 'none', | ||
allow: "none" | ||
}; | ||
const messages = { | ||
moveToNewLine: '`{{descriptor}}` must be placed on a new line', | ||
moveToNewLine: "`{{descriptor}}` must be placed on a new line" | ||
}; | ||
var jsxOneExpressionPerLine = { | ||
meta: { | ||
docs: { | ||
description: 'Require one JSX element per line', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('jsx-one-expression-per-line'), | ||
description: "Require one JSX element per line", | ||
category: "Stylistic Issues", | ||
url: utils.docsUrl("jsx-one-expression-per-line") | ||
}, | ||
fixable: 'whitespace', | ||
fixable: "whitespace", | ||
messages, | ||
schema: [ | ||
{ | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
allow: { | ||
enum: ['none', 'literal', 'single-child'], | ||
}, | ||
enum: ["none", "literal", "single-child"] | ||
} | ||
}, | ||
default: optionDefaults, | ||
additionalProperties: false, | ||
}, | ||
], | ||
additionalProperties: false | ||
} | ||
] | ||
}, | ||
create(context) { | ||
const options = Object.assign({}, optionDefaults, context.options[0]); | ||
function nodeKey(node) { | ||
return `${node.loc.start.line},${node.loc.start.column}` | ||
return `${node.loc.start.line},${node.loc.start.column}`; | ||
} | ||
function nodeDescriptor(n) { | ||
return n.openingElement ? n.openingElement.name.name : context.getSourceCode().getText(n).replace(/\n/g, '') | ||
return n.openingElement ? n.openingElement.name.name : context.getSourceCode().getText(n).replace(/\n/g, ""); | ||
} | ||
function handleJSX(node) { | ||
const children = node.children; | ||
if (!children || !children.length) | ||
return | ||
return; | ||
const openingElement = node.openingElement || node.openingFragment; | ||
@@ -73,56 +52,35 @@ const closingElement = node.closingElement || node.closingFragment; | ||
const closingElementEndLine = closingElement.loc.end.line; | ||
if (children.length === 1) { | ||
const child = children[0]; | ||
if ( | ||
openingElementStartLine === openingElementEndLine | ||
&& openingElementEndLine === closingElementStartLine | ||
&& closingElementStartLine === closingElementEndLine | ||
&& closingElementEndLine === child.loc.start.line | ||
&& child.loc.start.line === child.loc.end.line | ||
) { | ||
if ( | ||
options.allow === 'single-child' | ||
|| (options.allow === 'literal' && (child.type === 'Literal' || child.type === 'JSXText')) | ||
) | ||
return | ||
if (openingElementStartLine === openingElementEndLine && openingElementEndLine === closingElementStartLine && closingElementStartLine === closingElementEndLine && closingElementEndLine === child.loc.start.line && child.loc.start.line === child.loc.end.line) { | ||
if (options.allow === "single-child" || options.allow === "literal" && (child.type === "Literal" || child.type === "JSXText")) | ||
return; | ||
} | ||
} | ||
const childrenGroupedByLine = {}; | ||
const fixDetailsByNode = {}; | ||
children.forEach((child) => { | ||
let countNewLinesBeforeContent = 0; | ||
let countNewLinesAfterContent = 0; | ||
if (child.type === 'Literal' || child.type === 'JSXText') { | ||
if (child.type === "Literal" || child.type === "JSXText") { | ||
if (utils.isWhiteSpaces(child.raw)) | ||
return | ||
return; | ||
countNewLinesBeforeContent = (child.raw.match(/^\s*\n/g) || []).length; | ||
countNewLinesAfterContent = (child.raw.match(/\n\s*$/g) || []).length; | ||
} | ||
const startLine = child.loc.start.line + countNewLinesBeforeContent; | ||
const endLine = child.loc.end.line - countNewLinesAfterContent; | ||
if (startLine === endLine) { | ||
if (!childrenGroupedByLine[startLine]) | ||
childrenGroupedByLine[startLine] = []; | ||
childrenGroupedByLine[startLine].push(child); | ||
} | ||
else { | ||
} else { | ||
if (!childrenGroupedByLine[startLine]) | ||
childrenGroupedByLine[startLine] = []; | ||
childrenGroupedByLine[startLine].push(child); | ||
if (!childrenGroupedByLine[endLine]) | ||
childrenGroupedByLine[endLine] = []; | ||
childrenGroupedByLine[endLine].push(child); | ||
} | ||
}); | ||
Object.keys(childrenGroupedByLine).forEach((_line) => { | ||
@@ -132,15 +90,11 @@ const line = parseInt(_line, 10); | ||
const lastIndex = childrenGroupedByLine[line].length - 1; | ||
childrenGroupedByLine[line].forEach((child, i) => { | ||
let prevChild; | ||
let nextChild; | ||
if (i === firstIndex) { | ||
if (line === openingElementEndLine) | ||
prevChild = openingElement; | ||
} | ||
else { | ||
} else { | ||
prevChild = childrenGroupedByLine[line][i - 1]; | ||
} | ||
if (i === lastIndex) { | ||
@@ -150,18 +104,10 @@ if (line === closingElementStartLine) | ||
} | ||
function spaceBetweenPrev() { | ||
return ((prevChild.type === 'Literal' || prevChild.type === 'JSXText') && prevChild.raw.endsWith(' ')) | ||
|| ((child.type === 'Literal' || child.type === 'JSXText') && child.raw.startsWith(' ')) | ||
|| context.getSourceCode().isSpaceBetweenTokens(prevChild, child) | ||
return (prevChild.type === "Literal" || prevChild.type === "JSXText") && prevChild.raw.endsWith(" ") || (child.type === "Literal" || child.type === "JSXText") && child.raw.startsWith(" ") || context.getSourceCode().isSpaceBetweenTokens(prevChild, child); | ||
} | ||
function spaceBetweenNext() { | ||
return ((nextChild.type === 'Literal' || nextChild.type === 'JSXText') && nextChild.raw.startsWith(' ')) | ||
|| ((child.type === 'Literal' || child.type === 'JSXText') && child.raw.endsWith(' ')) | ||
|| context.getSourceCode().isSpaceBetweenTokens(child, nextChild) | ||
return (nextChild.type === "Literal" || nextChild.type === "JSXText") && nextChild.raw.startsWith(" ") || (child.type === "Literal" || child.type === "JSXText") && child.raw.endsWith(" ") || context.getSourceCode().isSpaceBetweenTokens(child, nextChild); | ||
} | ||
if (!prevChild && !nextChild) | ||
return | ||
return; | ||
const source = context.getSourceCode().getText(child); | ||
@@ -172,5 +118,3 @@ const leadingSpace = !!(prevChild && spaceBetweenPrev()); | ||
const trailingNewLine = !!nextChild; | ||
const key = nodeKey(child); | ||
if (!fixDetailsByNode[key]) { | ||
@@ -180,15 +124,11 @@ fixDetailsByNode[key] = { | ||
source, | ||
descriptor: nodeDescriptor(child), | ||
descriptor: nodeDescriptor(child) | ||
}; | ||
} | ||
if (leadingSpace) | ||
fixDetailsByNode[key].leadingSpace = true; | ||
if (leadingNewLine) | ||
fixDetailsByNode[key].leadingNewLine = true; | ||
if (trailingNewLine) | ||
fixDetailsByNode[key].trailingNewLine = true; | ||
if (trailingSpace) | ||
@@ -198,36 +138,30 @@ fixDetailsByNode[key].trailingSpace = true; | ||
}); | ||
Object.keys(fixDetailsByNode).forEach((key) => { | ||
const details = fixDetailsByNode[key]; | ||
const nodeToReport = details.node; | ||
const descriptor = details.descriptor; | ||
const source = details.source.replace(/(^ +| +(?=\n)*$)/g, ''); | ||
const leadingSpaceString = details.leadingSpace ? '\n{\' \'}' : ''; | ||
const trailingSpaceString = details.trailingSpace ? '{\' \'}\n' : ''; | ||
const leadingNewLineString = details.leadingNewLine ? '\n' : ''; | ||
const trailingNewLineString = details.trailingNewLine ? '\n' : ''; | ||
const source = details.source.replace(/(^ +| +(?=\n)*$)/g, ""); | ||
const leadingSpaceString = details.leadingSpace ? "\n{' '}" : ""; | ||
const trailingSpaceString = details.trailingSpace ? "{' '}\n" : ""; | ||
const leadingNewLineString = details.leadingNewLine ? "\n" : ""; | ||
const trailingNewLineString = details.trailingNewLine ? "\n" : ""; | ||
const replaceText = `${leadingSpaceString}${leadingNewLineString}${source}${trailingNewLineString}${trailingSpaceString}`; | ||
utils.report(context, messages.moveToNewLine, 'moveToNewLine', { | ||
utils.report(context, messages.moveToNewLine, "moveToNewLine", { | ||
node: nodeToReport, | ||
data: { | ||
descriptor, | ||
descriptor | ||
}, | ||
fix(fixer) { | ||
return fixer.replaceText(nodeToReport, replaceText) | ||
}, | ||
return fixer.replaceText(nodeToReport, replaceText); | ||
} | ||
}); | ||
}); | ||
} | ||
return { | ||
JSXElement: handleJSX, | ||
JSXFragment: handleJSX, | ||
} | ||
}, | ||
JSXFragment: handleJSX | ||
}; | ||
} | ||
}; | ||
exports.jsxOneExpressionPerLine = jsxOneExpressionPerLine; |
@@ -5,55 +5,34 @@ 'use strict'; | ||
/** | ||
* @fileoverview Disallow multiple spaces between inline JSX props | ||
* @author Adrian Moennich | ||
*/ | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
const messages = { | ||
noLineGap: 'Expected no line gap between “{{prop1}}” and “{{prop2}}”', | ||
onlyOneSpace: 'Expected only one space between “{{prop1}}” and “{{prop2}}”', | ||
noLineGap: "Expected no line gap between \u201C{{prop1}}\u201D and \u201C{{prop2}}\u201D", | ||
onlyOneSpace: "Expected only one space between \u201C{{prop1}}\u201D and \u201C{{prop2}}\u201D" | ||
}; | ||
var jsxPropsNoMultiSpaces = { | ||
meta: { | ||
docs: { | ||
description: 'Disallow multiple spaces between inline JSX props', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('jsx-props-no-multi-spaces'), | ||
description: "Disallow multiple spaces between inline JSX props", | ||
category: "Stylistic Issues", | ||
url: utils.docsUrl("jsx-props-no-multi-spaces") | ||
}, | ||
fixable: 'code', | ||
fixable: "code", | ||
messages, | ||
schema: [], | ||
schema: [] | ||
}, | ||
create(context) { | ||
const sourceCode = context.getSourceCode(); | ||
function getPropName(propNode) { | ||
switch (propNode.type) { | ||
case 'JSXSpreadAttribute': | ||
return context.getSourceCode().getText(propNode.argument) | ||
case 'JSXIdentifier': | ||
return propNode.name | ||
case 'JSXMemberExpression': | ||
return `${getPropName(propNode.object)}.${propNode.property.name}` | ||
case "JSXSpreadAttribute": | ||
return context.getSourceCode().getText(propNode.argument); | ||
case "JSXIdentifier": | ||
return propNode.name; | ||
case "JSXMemberExpression": | ||
return `${getPropName(propNode.object)}.${propNode.property.name}`; | ||
default: | ||
return propNode.name | ||
? propNode.name.name | ||
: `${context.getSourceCode().getText(propNode.object)}.${propNode.property.name}` // needed for typescript-eslint parser | ||
return propNode.name ? propNode.name.name : `${context.getSourceCode().getText(propNode.object)}.${propNode.property.name}`; | ||
} | ||
} | ||
// First and second must be adjacent nodes | ||
function hasEmptyLines(first, second) { | ||
const comments = sourceCode.getCommentsBefore ? sourceCode.getCommentsBefore(second) : []; | ||
const nodes = [].concat(first, comments, second); | ||
for (let i = 1; i < nodes.length; i += 1) { | ||
@@ -63,43 +42,36 @@ const prev = nodes[i - 1]; | ||
if (curr.loc.start.line - prev.loc.end.line >= 2) | ||
return true | ||
return true; | ||
} | ||
return false | ||
return false; | ||
} | ||
function checkSpacing(prev, node) { | ||
if (hasEmptyLines(prev, node)) { | ||
utils.report(context, messages.noLineGap, 'noLineGap', { | ||
utils.report(context, messages.noLineGap, "noLineGap", { | ||
node, | ||
data: { | ||
prop1: getPropName(prev), | ||
prop2: getPropName(node), | ||
}, | ||
prop2: getPropName(node) | ||
} | ||
}); | ||
} | ||
if (prev.loc.end.line !== node.loc.end.line) | ||
return | ||
return; | ||
const between = context.getSourceCode().text.slice(prev.range[1], node.range[0]); | ||
if (between !== ' ') { | ||
utils.report(context, messages.onlyOneSpace, 'onlyOneSpace', { | ||
if (between !== " ") { | ||
utils.report(context, messages.onlyOneSpace, "onlyOneSpace", { | ||
node, | ||
data: { | ||
prop1: getPropName(prev), | ||
prop2: getPropName(node), | ||
prop2: getPropName(node) | ||
}, | ||
fix(fixer) { | ||
return fixer.replaceTextRange([prev.range[1], node.range[0]], ' ') | ||
}, | ||
return fixer.replaceTextRange([prev.range[1], node.range[0]], " "); | ||
} | ||
}); | ||
} | ||
} | ||
function containsGenericType(node) { | ||
const containsTypeParams = typeof node.typeParameters !== 'undefined'; | ||
return containsTypeParams && node.typeParameters.type === 'TSTypeParameterInstantiation' | ||
const containsTypeParams = typeof node.typeParameters !== "undefined"; | ||
return containsTypeParams && node.typeParameters.type === "TSTypeParameterInstantiation"; | ||
} | ||
function getGenericNode(node) { | ||
@@ -109,3 +81,2 @@ const name = node.name; | ||
const type = node.typeParameters; | ||
return Object.assign( | ||
@@ -117,11 +88,9 @@ {}, | ||
name.range[0], | ||
type.range[1], | ||
], | ||
}, | ||
) | ||
type.range[1] | ||
] | ||
} | ||
); | ||
} | ||
return name | ||
return name; | ||
} | ||
return { | ||
@@ -131,9 +100,9 @@ JSXOpeningElement(node) { | ||
checkSpacing(prev, prop); | ||
return prop | ||
return prop; | ||
}, getGenericNode(node)); | ||
}, | ||
} | ||
}, | ||
} | ||
}; | ||
} | ||
}; | ||
exports.jsxPropsNoMultiSpaces = jsxPropsNoMultiSpaces; |
@@ -5,101 +5,63 @@ 'use strict'; | ||
/** | ||
* @fileoverview Prevent extra closing tags for components without children | ||
* @author Yannick Croissant | ||
*/ | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
const optionDefaults = { component: true, html: true }; | ||
const messages = { | ||
notSelfClosing: 'Empty components are self-closing', | ||
notSelfClosing: "Empty components are self-closing" | ||
}; | ||
var jsxSelfClosingComp = { | ||
meta: { | ||
docs: { | ||
description: 'Disallow extra closing tags for components without children', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('self-closing-comp'), | ||
description: "Disallow extra closing tags for components without children", | ||
category: "Stylistic Issues", | ||
url: utils.docsUrl("self-closing-comp") | ||
}, | ||
fixable: 'code', | ||
fixable: "code", | ||
messages, | ||
schema: [{ | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
component: { | ||
default: optionDefaults.component, | ||
type: 'boolean', | ||
type: "boolean" | ||
}, | ||
html: { | ||
default: optionDefaults.html, | ||
type: 'boolean', | ||
}, | ||
type: "boolean" | ||
} | ||
}, | ||
additionalProperties: false, | ||
}], | ||
additionalProperties: false | ||
}] | ||
}, | ||
create(context) { | ||
function isComponent(node) { | ||
return ( | ||
node.name | ||
&& (node.name.type === 'JSXIdentifier' || node.name.type === 'JSXMemberExpression') | ||
&& !utils.isDOMComponent(node) | ||
) | ||
return node.name && (node.name.type === "JSXIdentifier" || node.name.type === "JSXMemberExpression") && !utils.isDOMComponent(node); | ||
} | ||
function childrenIsEmpty(node) { | ||
return node.parent.children.length === 0 | ||
return node.parent.children.length === 0; | ||
} | ||
function childrenIsMultilineSpaces(node) { | ||
const childrens = node.parent.children; | ||
return ( | ||
childrens.length === 1 | ||
&& (childrens[0].type === 'Literal' || childrens[0].type === 'JSXText') | ||
&& childrens[0].value.includes('\n') | ||
&& childrens[0].value.replace(/(?!\xA0)\s/g, '') === '' | ||
) | ||
return childrens.length === 1 && (childrens[0].type === "Literal" || childrens[0].type === "JSXText") && childrens[0].value.includes("\n") && childrens[0].value.replace(/(?!\xA0)\s/g, "") === ""; | ||
} | ||
function isShouldBeSelfClosed(node) { | ||
const configuration = Object.assign({}, optionDefaults, context.options[0]); | ||
return ( | ||
(configuration.component && isComponent(node)) | ||
|| (configuration.html && utils.isDOMComponent(node)) | ||
) && !node.selfClosing && (childrenIsEmpty(node) || childrenIsMultilineSpaces(node)) | ||
return (configuration.component && isComponent(node) || configuration.html && utils.isDOMComponent(node)) && !node.selfClosing && (childrenIsEmpty(node) || childrenIsMultilineSpaces(node)); | ||
} | ||
return { | ||
JSXOpeningElement(node) { | ||
if (!isShouldBeSelfClosed(node)) | ||
return | ||
utils.report(context, messages.notSelfClosing, 'notSelfClosing', { | ||
return; | ||
utils.report(context, messages.notSelfClosing, "notSelfClosing", { | ||
node, | ||
fix(fixer) { | ||
// Represents the last character of the JSXOpeningElement, the '>' character | ||
const openingElementEnding = node.range[1] - 1; | ||
// Represents the last character of the JSXClosingElement, the '>' character | ||
const closingElementEnding = node.parent.closingElement.range[1]; | ||
// Replace />.*<\/.*>/ with '/>' | ||
const range = [openingElementEnding, closingElementEnding]; | ||
return fixer.replaceTextRange(range, ' />') | ||
}, | ||
return fixer.replaceTextRange(range, " />"); | ||
} | ||
}); | ||
}, | ||
} | ||
}, | ||
} | ||
}; | ||
} | ||
}; | ||
exports.jsxSelfClosingComp = jsxSelfClosingComp; |
@@ -5,66 +5,44 @@ 'use strict'; | ||
/** | ||
* @fileoverview Enforce props alphabetical sorting | ||
* @author Ilya Volodin, Yannick Croissant | ||
*/ | ||
const includes = (arr, value) => arr.includes(value); | ||
const toSorted = (arr, compareFn) => [...arr].sort(compareFn); | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
function isCallbackPropName(name) { | ||
return /^on[A-Z]/.test(name) | ||
return /^on[A-Z]/.test(name); | ||
} | ||
function isMultilineProp(node) { | ||
return node.loc.start.line !== node.loc.end.line | ||
return node.loc.start.line !== node.loc.end.line; | ||
} | ||
const messages = { | ||
noUnreservedProps: 'A customized reserved first list must only contain a subset of React reserved props. Remove: {{unreservedWords}}', | ||
listIsEmpty: 'A customized reserved first list must not be empty', | ||
listReservedPropsFirst: 'Reserved props must be listed before all other props', | ||
listCallbacksLast: 'Callbacks must be listed after all other props', | ||
listShorthandFirst: 'Shorthand props must be listed before all other props', | ||
listShorthandLast: 'Shorthand props must be listed after all other props', | ||
listMultilineFirst: 'Multiline props must be listed before all other props', | ||
listMultilineLast: 'Multiline props must be listed after all other props', | ||
sortPropsByAlpha: 'Props should be sorted alphabetically', | ||
noUnreservedProps: "A customized reserved first list must only contain a subset of React reserved props. Remove: {{unreservedWords}}", | ||
listIsEmpty: "A customized reserved first list must not be empty", | ||
listReservedPropsFirst: "Reserved props must be listed before all other props", | ||
listCallbacksLast: "Callbacks must be listed after all other props", | ||
listShorthandFirst: "Shorthand props must be listed before all other props", | ||
listShorthandLast: "Shorthand props must be listed after all other props", | ||
listMultilineFirst: "Multiline props must be listed before all other props", | ||
listMultilineLast: "Multiline props must be listed after all other props", | ||
sortPropsByAlpha: "Props should be sorted alphabetically" | ||
}; | ||
const RESERVED_PROPS_LIST = [ | ||
'children', | ||
'dangerouslySetInnerHTML', | ||
'key', | ||
'ref', | ||
"children", | ||
"dangerouslySetInnerHTML", | ||
"key", | ||
"ref" | ||
]; | ||
function isReservedPropName(name, list) { | ||
return list.includes(name) | ||
return list.includes(name); | ||
} | ||
let attributeMap; | ||
// attributeMap = { end: endrange, hasComment: true||false if comment in between nodes exists, it needs to be sorted to end } | ||
function shouldSortToEnd(node) { | ||
const attr = attributeMap.get(node); | ||
return !!attr && !!attr.hasComment | ||
return !!attr && !!attr.hasComment; | ||
} | ||
function contextCompare(a, b, options) { | ||
let aProp = utils.getPropName(a); | ||
let bProp = utils.getPropName(b); | ||
const aSortToEnd = shouldSortToEnd(a); | ||
const bSortToEnd = shouldSortToEnd(b); | ||
if (aSortToEnd && !bSortToEnd) | ||
return 1 | ||
return 1; | ||
if (!aSortToEnd && bSortToEnd) | ||
return -1 | ||
return -1; | ||
if (options.reservedFirst) { | ||
@@ -74,8 +52,6 @@ const aIsReserved = isReservedPropName(aProp, options.reservedList); | ||
if (aIsReserved && !bIsReserved) | ||
return -1 | ||
return -1; | ||
if (!aIsReserved && bIsReserved) | ||
return 1 | ||
return 1; | ||
} | ||
if (options.callbacksLast) { | ||
@@ -85,57 +61,38 @@ const aIsCallback = isCallbackPropName(aProp); | ||
if (aIsCallback && !bIsCallback) | ||
return 1 | ||
return 1; | ||
if (!aIsCallback && bIsCallback) | ||
return -1 | ||
return -1; | ||
} | ||
if (options.shorthandFirst || options.shorthandLast) { | ||
const shorthandSign = options.shorthandFirst ? -1 : 1; | ||
if (!a.value && b.value) | ||
return shorthandSign | ||
return shorthandSign; | ||
if (a.value && !b.value) | ||
return -shorthandSign | ||
return -shorthandSign; | ||
} | ||
if (options.multiline !== 'ignore') { | ||
const multilineSign = options.multiline === 'first' ? -1 : 1; | ||
if (options.multiline !== "ignore") { | ||
const multilineSign = options.multiline === "first" ? -1 : 1; | ||
const aIsMultiline = isMultilineProp(a); | ||
const bIsMultiline = isMultilineProp(b); | ||
if (aIsMultiline && !bIsMultiline) | ||
return multilineSign | ||
return multilineSign; | ||
if (!aIsMultiline && bIsMultiline) | ||
return -multilineSign | ||
return -multilineSign; | ||
} | ||
if (options.noSortAlphabetically) | ||
return 0 | ||
const actualLocale = options.locale === 'auto' ? undefined : options.locale; | ||
return 0; | ||
const actualLocale = options.locale === "auto" ? void 0 : options.locale; | ||
if (options.ignoreCase) { | ||
aProp = aProp.toLowerCase(); | ||
bProp = bProp.toLowerCase(); | ||
return aProp.localeCompare(bProp, actualLocale) | ||
return aProp.localeCompare(bProp, actualLocale); | ||
} | ||
if (aProp === bProp) | ||
return 0 | ||
if (options.locale === 'auto') | ||
return aProp < bProp ? -1 : 1 | ||
return aProp.localeCompare(bProp, actualLocale) | ||
return 0; | ||
if (options.locale === "auto") | ||
return aProp < bProp ? -1 : 1; | ||
return aProp.localeCompare(bProp, actualLocale); | ||
} | ||
/** | ||
* Create an array of arrays where each subarray is composed of attributes | ||
* that are considered sortable. | ||
* @param {Array<JSXSpreadAttribute|JSXAttribute>} attributes | ||
* @param {object} context The context of the rule | ||
* @return {Array<Array<JSXAttribute>>} | ||
*/ | ||
function getGroupsOfSortableAttributes(attributes, context) { | ||
const sourceCode = context.getSourceCode(); | ||
const sortableAttributeGroups = []; | ||
@@ -146,3 +103,2 @@ let groupCount = 0; | ||
} | ||
for (let i = 0; i < attributes.length; i++) { | ||
@@ -155,14 +111,7 @@ const attribute = attributes[i]; | ||
comment = sourceCode.getCommentsAfter(attribute); | ||
} catch (e) { | ||
} | ||
catch (e) { /**/ } | ||
const lastAttr = attributes[i - 1]; | ||
const attrIsSpread = attribute.type === 'JSXSpreadAttribute'; | ||
// If we have no groups or if the last attribute was JSXSpreadAttribute | ||
// then we start a new group. Append attributes to the group until we | ||
// come across another JSXSpreadAttribute or exhaust the array. | ||
if ( | ||
!lastAttr | ||
|| (lastAttr.type === 'JSXSpreadAttribute' && !attrIsSpread) | ||
) { | ||
const attrIsSpread = attribute.type === "JSXSpreadAttribute"; | ||
if (!lastAttr || lastAttr.type === "JSXSpreadAttribute" && !attrIsSpread) { | ||
groupCount += 1; | ||
@@ -175,4 +124,3 @@ sortableAttributeGroups[groupCount - 1] = []; | ||
addtoSortableAttributeGroups(attribute); | ||
} | ||
else { | ||
} else { | ||
const firstComment = comment[0]; | ||
@@ -185,12 +133,9 @@ const commentline = firstComment.loc.start.line; | ||
i += 1; | ||
} | ||
else if (attributeline === commentline) { | ||
if (firstComment.type === 'Block' && nextAttribute) { | ||
} else if (attributeline === commentline) { | ||
if (firstComment.type === "Block" && nextAttribute) { | ||
attributeMap.set(attribute, { end: nextAttribute.range[1], hasComment: true }); | ||
i += 1; | ||
} | ||
else if (firstComment.type === 'Block') { | ||
} else if (firstComment.type === "Block") { | ||
attributeMap.set(attribute, { end: firstComment.range[1], hasComment: true }); | ||
} | ||
else { | ||
} else { | ||
attributeMap.set(attribute, { end: firstComment.range[1], hasComment: false }); | ||
@@ -200,12 +145,7 @@ } | ||
} | ||
} | ||
else if (comment.length > 1 && attributeline + 1 === comment[1].loc.start.line && nextAttribute) { | ||
} else if (comment.length > 1 && attributeline + 1 === comment[1].loc.start.line && nextAttribute) { | ||
const commentNextAttribute = sourceCode.getCommentsAfter(nextAttribute); | ||
attributeMap.set(attribute, { end: nextAttribute.range[1], hasComment: true }); | ||
if ( | ||
commentNextAttribute.length === 1 | ||
&& nextAttribute.loc.start.line === commentNextAttribute[0].loc.start.line | ||
) | ||
if (commentNextAttribute.length === 1 && nextAttribute.loc.start.line === commentNextAttribute[0].loc.start.line) | ||
attributeMap.set(attribute, { end: commentNextAttribute[0].range[1], hasComment: true }); | ||
addtoSortableAttributeGroups(attribute); | ||
@@ -217,5 +157,4 @@ i += 1; | ||
} | ||
return sortableAttributeGroups | ||
return sortableAttributeGroups; | ||
} | ||
function generateFixerFunction(node, context, reservedList) { | ||
@@ -229,10 +168,6 @@ const sourceCode = context.getSourceCode(); | ||
const shorthandLast = configuration.shorthandLast || false; | ||
const multiline = configuration.multiline || 'ignore'; | ||
const multiline = configuration.multiline || "ignore"; | ||
const noSortAlphabetically = configuration.noSortAlphabetically || false; | ||
const reservedFirst = configuration.reservedFirst || false; | ||
const locale = configuration.locale || 'auto'; | ||
// Sort props according to the context. Only supports ignoreCase. | ||
// Since we cannot safely move JSXSpreadAttribute (due to potential variable overrides), | ||
// we only consider groups of sortable attributes. | ||
const locale = configuration.locale || "auto"; | ||
const options = { | ||
@@ -247,13 +182,9 @@ ignoreCase, | ||
reservedList, | ||
locale, | ||
locale | ||
}; | ||
const sortableAttributeGroups = getGroupsOfSortableAttributes(attributes, context); | ||
const sortedAttributeGroups = sortableAttributeGroups | ||
.slice(0) | ||
.map(group => toSorted(group, (a, b) => contextCompare(a, b, options))); | ||
const sortedAttributeGroups = sortableAttributeGroups.slice(0).map((group) => toSorted(group, (a, b) => contextCompare(a, b, options))); | ||
return function fixFunction(fixer) { | ||
const fixers = []; | ||
let source = sourceCode.getText(); | ||
sortableAttributeGroups.forEach((sortableGroup, ii) => { | ||
@@ -265,9 +196,7 @@ sortableGroup.forEach((attr, jj) => { | ||
range: [attr.range[0], attributeMap.get(attr).end], | ||
text: sortedAttrText, | ||
text: sortedAttrText | ||
}); | ||
}); | ||
}); | ||
fixers.sort((a, b) => b.range[0] - a.range[0]); | ||
const firstFixer = fixers[0]; | ||
@@ -277,43 +206,31 @@ const lastFixer = fixers[fixers.length - 1]; | ||
const rangeEnd = firstFixer ? firstFixer.range[1] : -0; | ||
fixers.forEach((fix) => { | ||
source = `${source.slice(0, fix.range[0])}${fix.text}${source.slice(fix.range[1])}`; | ||
}); | ||
return fixer.replaceTextRange([rangeStart, rangeEnd], source.slice(rangeStart, rangeEnd)) | ||
} | ||
return fixer.replaceTextRange([rangeStart, rangeEnd], source.slice(rangeStart, rangeEnd)); | ||
}; | ||
} | ||
/** | ||
* Checks if the `reservedFirst` option is valid | ||
* @param {object} context The context of the rule | ||
* @param {boolean | Array<string>} reservedFirst The `reservedFirst` option | ||
* @return {Function|undefined} If an error is detected, a function to generate the error message, otherwise, `undefined` | ||
*/ | ||
function validateReservedFirstConfig(context, reservedFirst) { | ||
if (reservedFirst) { | ||
if (Array.isArray(reservedFirst)) { | ||
// Only allow a subset of reserved words in customized lists | ||
const nonReservedWords = reservedFirst.filter(word => !isReservedPropName( | ||
const nonReservedWords = reservedFirst.filter((word) => !isReservedPropName( | ||
word, | ||
RESERVED_PROPS_LIST, | ||
RESERVED_PROPS_LIST | ||
)); | ||
if (reservedFirst.length === 0) { | ||
return function Report(decl) { | ||
utils.report(context, messages.listIsEmpty, 'listIsEmpty', { | ||
node: decl, | ||
utils.report(context, messages.listIsEmpty, "listIsEmpty", { | ||
node: decl | ||
}); | ||
} | ||
}; | ||
} | ||
if (nonReservedWords.length > 0) { | ||
return function Report(decl) { | ||
utils.report(context, messages.noUnreservedProps, 'noUnreservedProps', { | ||
utils.report(context, messages.noUnreservedProps, "noUnreservedProps", { | ||
node: decl, | ||
data: { | ||
unreservedWords: nonReservedWords.toString(), | ||
}, | ||
unreservedWords: nonReservedWords.toString() | ||
} | ||
}); | ||
} | ||
}; | ||
} | ||
@@ -323,44 +240,25 @@ } | ||
} | ||
const reportedNodeAttributes = new WeakMap(); | ||
/** | ||
* Check if the current node attribute has already been reported with the same error type | ||
* if that's the case then we don't report a new error | ||
* otherwise we report the error | ||
* @param {object} nodeAttribute The node attribute to be reported | ||
* @param {string} errorType The error type to be reported | ||
* @param {object} node The parent node for the node attribute | ||
* @param {object} context The context of the rule | ||
* @param {Array<string>} reservedList The list of reserved props | ||
*/ | ||
const reportedNodeAttributes = /* @__PURE__ */ new WeakMap(); | ||
function reportNodeAttribute(nodeAttribute, errorType, node, context, reservedList) { | ||
const errors = reportedNodeAttributes.get(nodeAttribute) || []; | ||
if (includes(errors, errorType)) | ||
return | ||
return; | ||
errors.push(errorType); | ||
reportedNodeAttributes.set(nodeAttribute, errors); | ||
utils.report(context, messages[errorType], errorType, { | ||
node: nodeAttribute.name, | ||
fix: generateFixerFunction(node, context, reservedList), | ||
fix: generateFixerFunction(node, context, reservedList) | ||
}); | ||
} | ||
var jsxSortProps = { | ||
meta: { | ||
docs: { | ||
description: 'Enforce props alphabetical sorting', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('jsx-sort-props'), | ||
description: "Enforce props alphabetical sorting", | ||
category: "Stylistic Issues", | ||
url: utils.docsUrl("jsx-sort-props") | ||
}, | ||
fixable: 'code', | ||
fixable: "code", | ||
messages, | ||
schema: [{ | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
@@ -370,36 +268,35 @@ // Whether callbacks (prefixed with "on") should be listed at the very end, | ||
callbacksLast: { | ||
type: 'boolean', | ||
type: "boolean" | ||
}, | ||
// Whether shorthand properties (without a value) should be listed first | ||
shorthandFirst: { | ||
type: 'boolean', | ||
type: "boolean" | ||
}, | ||
// Whether shorthand properties (without a value) should be listed last | ||
shorthandLast: { | ||
type: 'boolean', | ||
type: "boolean" | ||
}, | ||
// Whether multiline properties should be listed first or last | ||
multiline: { | ||
enum: ['ignore', 'first', 'last'], | ||
default: 'ignore', | ||
enum: ["ignore", "first", "last"], | ||
default: "ignore" | ||
}, | ||
ignoreCase: { | ||
type: 'boolean', | ||
type: "boolean" | ||
}, | ||
// Whether alphabetical sorting should be enforced | ||
noSortAlphabetically: { | ||
type: 'boolean', | ||
type: "boolean" | ||
}, | ||
reservedFirst: { | ||
type: ['array', 'boolean'], | ||
type: ["array", "boolean"] | ||
}, | ||
locale: { | ||
type: 'string', | ||
default: 'auto', | ||
}, | ||
type: "string", | ||
default: "auto" | ||
} | ||
}, | ||
additionalProperties: false, | ||
}], | ||
additionalProperties: false | ||
}] | ||
}, | ||
create(context) { | ||
@@ -411,3 +308,3 @@ const configuration = context.options[0] || {}; | ||
const shorthandLast = configuration.shorthandLast || false; | ||
const multiline = configuration.multiline || 'ignore'; | ||
const multiline = configuration.multiline || "ignore"; | ||
const noSortAlphabetically = configuration.noSortAlphabetically || false; | ||
@@ -417,17 +314,12 @@ const reservedFirst = configuration.reservedFirst || false; | ||
const reservedList = Array.isArray(reservedFirst) ? reservedFirst : RESERVED_PROPS_LIST; | ||
const locale = configuration.locale || 'auto'; | ||
const locale = configuration.locale || "auto"; | ||
return { | ||
Program() { | ||
attributeMap = new WeakMap(); | ||
attributeMap = /* @__PURE__ */ new WeakMap(); | ||
}, | ||
JSXOpeningElement(node) { | ||
// `dangerouslySetInnerHTML` is only "reserved" on DOM components | ||
const nodeReservedList = reservedFirst && !utils.isDOMComponent(node) ? reservedList.filter(prop => prop !== 'dangerouslySetInnerHTML') : reservedList; | ||
const nodeReservedList = reservedFirst && !utils.isDOMComponent(node) ? reservedList.filter((prop) => prop !== "dangerouslySetInnerHTML") : reservedList; | ||
node.attributes.reduce((memo, decl, idx, attrs) => { | ||
if (decl.type === 'JSXSpreadAttribute') | ||
return attrs[idx + 1] | ||
if (decl.type === "JSXSpreadAttribute") | ||
return attrs[idx + 1]; | ||
let previousPropName = utils.getPropName(memo); | ||
@@ -439,3 +331,2 @@ let currentPropName = utils.getPropName(decl); | ||
const currentIsCallback = isCallbackPropName(currentPropName); | ||
if (ignoreCase) { | ||
@@ -445,104 +336,71 @@ previousPropName = previousPropName.toLowerCase(); | ||
} | ||
if (reservedFirst) { | ||
if (reservedFirstError) { | ||
reservedFirstError(decl); | ||
return memo | ||
return memo; | ||
} | ||
const previousIsReserved = isReservedPropName(previousPropName, nodeReservedList); | ||
const currentIsReserved = isReservedPropName(currentPropName, nodeReservedList); | ||
if (previousIsReserved && !currentIsReserved) | ||
return decl | ||
return decl; | ||
if (!previousIsReserved && currentIsReserved) { | ||
reportNodeAttribute(decl, 'listReservedPropsFirst', node, context, nodeReservedList); | ||
return memo | ||
reportNodeAttribute(decl, "listReservedPropsFirst", node, context, nodeReservedList); | ||
return memo; | ||
} | ||
} | ||
if (callbacksLast) { | ||
if (!previousIsCallback && currentIsCallback) { | ||
// Entering the callback prop section | ||
return decl | ||
return decl; | ||
} | ||
if (previousIsCallback && !currentIsCallback) { | ||
// Encountered a non-callback prop after a callback prop | ||
reportNodeAttribute(memo, 'listCallbacksLast', node, context, nodeReservedList); | ||
return memo | ||
reportNodeAttribute(memo, "listCallbacksLast", node, context, nodeReservedList); | ||
return memo; | ||
} | ||
} | ||
if (shorthandFirst) { | ||
if (currentValue && !previousValue) | ||
return decl | ||
return decl; | ||
if (!currentValue && previousValue) { | ||
reportNodeAttribute(decl, 'listShorthandFirst', node, context, nodeReservedList); | ||
return memo | ||
reportNodeAttribute(decl, "listShorthandFirst", node, context, nodeReservedList); | ||
return memo; | ||
} | ||
} | ||
if (shorthandLast) { | ||
if (!currentValue && previousValue) | ||
return decl | ||
return decl; | ||
if (currentValue && !previousValue) { | ||
reportNodeAttribute(memo, 'listShorthandLast', node, context, nodeReservedList); | ||
return memo | ||
reportNodeAttribute(memo, "listShorthandLast", node, context, nodeReservedList); | ||
return memo; | ||
} | ||
} | ||
const previousIsMultiline = isMultilineProp(memo); | ||
const currentIsMultiline = isMultilineProp(decl); | ||
if (multiline === 'first') { | ||
if (multiline === "first") { | ||
if (previousIsMultiline && !currentIsMultiline) { | ||
// Exiting the multiline prop section | ||
return decl | ||
return decl; | ||
} | ||
if (!previousIsMultiline && currentIsMultiline) { | ||
// Encountered a non-multiline prop before a multiline prop | ||
reportNodeAttribute(decl, 'listMultilineFirst', node, context, nodeReservedList); | ||
return memo | ||
reportNodeAttribute(decl, "listMultilineFirst", node, context, nodeReservedList); | ||
return memo; | ||
} | ||
} | ||
else if (multiline === 'last') { | ||
} else if (multiline === "last") { | ||
if (!previousIsMultiline && currentIsMultiline) { | ||
// Entering the multiline prop section | ||
return decl | ||
return decl; | ||
} | ||
if (previousIsMultiline && !currentIsMultiline) { | ||
// Encountered a non-multiline prop after a multiline prop | ||
reportNodeAttribute(memo, 'listMultilineLast', node, context, nodeReservedList); | ||
return memo | ||
reportNodeAttribute(memo, "listMultilineLast", node, context, nodeReservedList); | ||
return memo; | ||
} | ||
} | ||
if ( | ||
!noSortAlphabetically | ||
&& ( | ||
(ignoreCase || locale !== 'auto') | ||
? previousPropName.localeCompare(currentPropName, locale === 'auto' ? undefined : locale) > 0 | ||
: previousPropName > currentPropName | ||
) | ||
) { | ||
reportNodeAttribute(decl, 'sortPropsByAlpha', node, context, nodeReservedList); | ||
return memo | ||
if (!noSortAlphabetically && (ignoreCase || locale !== "auto" ? previousPropName.localeCompare(currentPropName, locale === "auto" ? void 0 : locale) > 0 : previousPropName > currentPropName)) { | ||
reportNodeAttribute(decl, "sortPropsByAlpha", node, context, nodeReservedList); | ||
return memo; | ||
} | ||
return decl | ||
return decl; | ||
}, node.attributes[0]); | ||
}, | ||
} | ||
}, | ||
} | ||
}; | ||
} | ||
}; | ||
exports.jsxSortProps = jsxSortProps; |
@@ -5,93 +5,73 @@ 'use strict'; | ||
/** | ||
* @fileoverview Validates whitespace in and around the JSX opening and closing brackets | ||
* @author Diogo Franco (Kovensky) | ||
*/ | ||
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', | ||
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 (option === "never") { | ||
if (!adjacent) { | ||
utils.report(context, messages.selfCloseSlashNoSpace, 'selfCloseSlashNoSpace', { | ||
utils.report(context, messages.selfCloseSlashNoSpace, "selfCloseSlashNoSpace", { | ||
node, | ||
loc: { | ||
start: lastTokens[0].loc.start, | ||
end: lastTokens[1].loc.end, | ||
end: lastTokens[1].loc.end | ||
}, | ||
fix(fixer) { | ||
return fixer.removeRange([lastTokens[0].range[1], lastTokens[1].range[0]]) | ||
}, | ||
return fixer.removeRange([lastTokens[0].range[1], lastTokens[1].range[0]]); | ||
} | ||
}); | ||
} | ||
} | ||
else if (option === 'always' && adjacent) { | ||
utils.report(context, messages.selfCloseSlashNeedSpace, 'selfCloseSlashNeedSpace', { | ||
} else if (option === "always" && adjacent) { | ||
utils.report(context, messages.selfCloseSlashNeedSpace, "selfCloseSlashNeedSpace", { | ||
node, | ||
loc: { | ||
start: lastTokens[0].loc.start, | ||
end: lastTokens[1].loc.end, | ||
end: lastTokens[1].loc.end | ||
}, | ||
fix(fixer) { | ||
return fixer.insertTextBefore(lastTokens[1], ' ') | ||
}, | ||
return fixer.insertTextBefore(lastTokens[1], " "); | ||
} | ||
}); | ||
} | ||
} | ||
else { | ||
} else { | ||
const firstTokens = sourceCode.getFirstTokens(node, 2); | ||
adjacent = !sourceCode.isSpaceBetweenTokens(firstTokens[0], firstTokens[1]); | ||
if (option === 'never') { | ||
if (option === "never") { | ||
if (!adjacent) { | ||
utils.report(context, messages.closeSlashNoSpace, 'closeSlashNoSpace', { | ||
utils.report(context, messages.closeSlashNoSpace, "closeSlashNoSpace", { | ||
node, | ||
loc: { | ||
start: firstTokens[0].loc.start, | ||
end: firstTokens[1].loc.end, | ||
end: firstTokens[1].loc.end | ||
}, | ||
fix(fixer) { | ||
return fixer.removeRange([firstTokens[0].range[1], firstTokens[1].range[0]]) | ||
}, | ||
return fixer.removeRange([firstTokens[0].range[1], firstTokens[1].range[0]]); | ||
} | ||
}); | ||
} | ||
} | ||
else if (option === 'always' && adjacent) { | ||
utils.report(context, messages.closeSlashNeedSpace, 'closeSlashNeedSpace', { | ||
} else if (option === "always" && adjacent) { | ||
utils.report(context, messages.closeSlashNeedSpace, "closeSlashNeedSpace", { | ||
node, | ||
loc: { | ||
start: firstTokens[0].loc.start, | ||
end: firstTokens[1].loc.end, | ||
end: firstTokens[1].loc.end | ||
}, | ||
fix(fixer) { | ||
return fixer.insertTextBefore(firstTokens[1], ' ') | ||
}, | ||
return fixer.insertTextBefore(firstTokens[1], " "); | ||
} | ||
}); | ||
@@ -101,3 +81,2 @@ } | ||
} | ||
function validateBeforeSelfClosing(context, node, option) { | ||
@@ -107,32 +86,27 @@ const sourceCode = context.getSourceCode(); | ||
const closingSlash = sourceCode.getTokenAfter(leftToken); | ||
if (node.loc.start.line !== node.loc.end.line && option === 'proportional-always') { | ||
if (node.loc.start.line !== node.loc.end.line && option === "proportional-always") { | ||
if (leftToken.loc.end.line === closingSlash.loc.start.line) { | ||
utils.report(context, messages.beforeSelfCloseNeedNewline, 'beforeSelfCloseNeedNewline', { | ||
utils.report(context, messages.beforeSelfCloseNeedNewline, "beforeSelfCloseNeedNewline", { | ||
node, | ||
loc: leftToken.loc.end, | ||
fix(fixer) { | ||
return fixer.insertTextBefore(closingSlash, '\n') | ||
}, | ||
return fixer.insertTextBefore(closingSlash, "\n"); | ||
} | ||
}); | ||
return | ||
return; | ||
} | ||
} | ||
if (leftToken.loc.end.line !== closingSlash.loc.start.line) | ||
return | ||
return; | ||
const adjacent = !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash); | ||
if ((option === 'always' || option === 'proportional-always') && adjacent) { | ||
utils.report(context, messages.beforeSelfCloseNeedSpace, 'beforeSelfCloseNeedSpace', { | ||
if ((option === "always" || option === "proportional-always") && adjacent) { | ||
utils.report(context, messages.beforeSelfCloseNeedSpace, "beforeSelfCloseNeedSpace", { | ||
node, | ||
loc: closingSlash.loc.start, | ||
fix(fixer) { | ||
return fixer.insertTextBefore(closingSlash, ' ') | ||
}, | ||
return fixer.insertTextBefore(closingSlash, " "); | ||
} | ||
}); | ||
} | ||
else if (option === 'never' && !adjacent) { | ||
utils.report(context, messages.beforeSelfCloseNoSpace, 'beforeSelfCloseNoSpace', { | ||
} else if (option === "never" && !adjacent) { | ||
utils.report(context, messages.beforeSelfCloseNoSpace, "beforeSelfCloseNoSpace", { | ||
node, | ||
@@ -142,108 +116,93 @@ loc: closingSlash.loc.start, | ||
const previousToken = sourceCode.getTokenBefore(closingSlash); | ||
return fixer.removeRange([previousToken.range[1], closingSlash.range[0]]) | ||
}, | ||
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 (option === "allow-multiline") { | ||
if (openingToken.loc.start.line !== node.name.loc.start.line) | ||
return | ||
return; | ||
} | ||
const adjacent = !sourceCode.isSpaceBetweenTokens(openingToken, node.name); | ||
if (option === 'never' || option === 'allow-multiline') { | ||
if (option === "never" || option === "allow-multiline") { | ||
if (!adjacent) { | ||
utils.report(context, messages.afterOpenNoSpace, 'afterOpenNoSpace', { | ||
utils.report(context, messages.afterOpenNoSpace, "afterOpenNoSpace", { | ||
node, | ||
loc: { | ||
start: openingToken.loc.start, | ||
end: node.name.loc.start, | ||
end: node.name.loc.start | ||
}, | ||
fix(fixer) { | ||
return fixer.removeRange([openingToken.range[1], node.name.range[0]]) | ||
}, | ||
return fixer.removeRange([openingToken.range[1], node.name.range[0]]); | ||
} | ||
}); | ||
} | ||
} | ||
else if (option === 'always' && adjacent) { | ||
utils.report(context, messages.afterOpenNeedSpace, 'afterOpenNeedSpace', { | ||
} else if (option === "always" && adjacent) { | ||
utils.report(context, messages.afterOpenNeedSpace, "afterOpenNeedSpace", { | ||
node, | ||
loc: { | ||
start: openingToken.loc.start, | ||
end: node.name.loc.start, | ||
end: node.name.loc.start | ||
}, | ||
fix(fixer) { | ||
return fixer.insertTextBefore(node.name, ' ') | ||
}, | ||
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' | ||
? utils.getTokenBeforeClosingBracket(node) | ||
: sourceCode.getLastTokens(node, 2)[0]; | ||
const leftToken = option === "proportional-always" ? utils.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 (node.loc.start.line !== node.loc.end.line && option === "proportional-always") { | ||
if (leftToken.loc.end.line === closingToken.loc.start.line) { | ||
utils.report(context, messages.beforeCloseNeedNewline, 'beforeCloseNeedNewline', { | ||
utils.report(context, messages.beforeCloseNeedNewline, "beforeCloseNeedNewline", { | ||
node, | ||
loc: leftToken.loc.end, | ||
fix(fixer) { | ||
return fixer.insertTextBefore(closingToken, '\n') | ||
}, | ||
return fixer.insertTextBefore(closingToken, "\n"); | ||
} | ||
}); | ||
return | ||
return; | ||
} | ||
} | ||
if (leftToken.loc.start.line !== closingToken.loc.start.line) | ||
return | ||
return; | ||
const adjacent = !sourceCode.isSpaceBetweenTokens(leftToken, closingToken); | ||
if (option === 'never' && !adjacent) { | ||
utils.report(context, messages.beforeCloseNoSpace, 'beforeCloseNoSpace', { | ||
if (option === "never" && !adjacent) { | ||
utils.report(context, messages.beforeCloseNoSpace, "beforeCloseNoSpace", { | ||
node, | ||
loc: { | ||
start: leftToken.loc.end, | ||
end: closingToken.loc.start, | ||
end: closingToken.loc.start | ||
}, | ||
fix(fixer) { | ||
return fixer.removeRange([leftToken.range[1], closingToken.range[0]]) | ||
}, | ||
return fixer.removeRange([leftToken.range[1], closingToken.range[0]]); | ||
} | ||
}); | ||
} | ||
else if (option === 'always' && adjacent) { | ||
utils.report(context, messages.beforeCloseNeedSpace, 'beforeCloseNeedSpace', { | ||
} else if (option === "always" && adjacent) { | ||
utils.report(context, messages.beforeCloseNeedSpace, "beforeCloseNeedSpace", { | ||
node, | ||
loc: { | ||
start: leftToken.loc.end, | ||
end: closingToken.loc.start, | ||
end: closingToken.loc.start | ||
}, | ||
fix(fixer) { | ||
return fixer.insertTextBefore(closingToken, ' ') | ||
}, | ||
return fixer.insertTextBefore(closingToken, " "); | ||
} | ||
}); | ||
} | ||
else if (option === 'proportional-always' && node.type === 'JSXOpeningElement' && adjacent !== (node.loc.start.line === node.loc.end.line)) { | ||
utils.report(context, messages.beforeCloseNeedSpace, 'beforeCloseNeedSpace', { | ||
} else if (option === "proportional-always" && node.type === "JSXOpeningElement" && adjacent !== (node.loc.start.line === node.loc.end.line)) { | ||
utils.report(context, messages.beforeCloseNeedSpace, "beforeCloseNeedSpace", { | ||
node, | ||
loc: { | ||
start: leftToken.loc.end, | ||
end: closingToken.loc.start, | ||
end: closingToken.loc.start | ||
}, | ||
fix(fixer) { | ||
return fixer.insertTextBefore(closingToken, ' ') | ||
}, | ||
return fixer.insertTextBefore(closingToken, " "); | ||
} | ||
}); | ||
@@ -253,79 +212,64 @@ } | ||
} | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
const optionDefaults = { | ||
closingSlash: 'never', | ||
beforeSelfClosing: 'always', | ||
afterOpening: 'never', | ||
beforeClosing: 'allow', | ||
closingSlash: "never", | ||
beforeSelfClosing: "always", | ||
afterOpening: "never", | ||
beforeClosing: "allow" | ||
}; | ||
var jsxTagSpacing = { | ||
meta: { | ||
docs: { | ||
description: 'Enforce whitespace in and around the JSX opening and closing brackets', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('jsx-tag-spacing'), | ||
description: "Enforce whitespace in and around the JSX opening and closing brackets", | ||
category: "Stylistic Issues", | ||
url: utils.docsUrl("jsx-tag-spacing") | ||
}, | ||
fixable: 'whitespace', | ||
fixable: "whitespace", | ||
messages, | ||
schema: [ | ||
{ | ||
type: 'object', | ||
type: "object", | ||
properties: { | ||
closingSlash: { | ||
enum: ['always', 'never', 'allow'], | ||
enum: ["always", "never", "allow"] | ||
}, | ||
beforeSelfClosing: { | ||
enum: ['always', 'proportional-always', 'never', 'allow'], | ||
enum: ["always", "proportional-always", "never", "allow"] | ||
}, | ||
afterOpening: { | ||
enum: ['always', 'allow-multiline', 'never', 'allow'], | ||
enum: ["always", "allow-multiline", "never", "allow"] | ||
}, | ||
beforeClosing: { | ||
enum: ['always', 'proportional-always', 'never', 'allow'], | ||
}, | ||
enum: ["always", "proportional-always", "never", "allow"] | ||
} | ||
}, | ||
default: optionDefaults, | ||
additionalProperties: false, | ||
}, | ||
], | ||
additionalProperties: false | ||
} | ||
] | ||
}, | ||
create(context) { | ||
const options = Object.assign({}, optionDefaults, context.options[0]); | ||
return { | ||
JSXOpeningElement(node) { | ||
if (options.closingSlash !== 'allow' && node.selfClosing) | ||
if (options.closingSlash !== "allow" && node.selfClosing) | ||
validateClosingSlash(context, node, options.closingSlash); | ||
if (options.afterOpening !== 'allow') | ||
if (options.afterOpening !== "allow") | ||
validateAfterOpening(context, node, options.afterOpening); | ||
if (options.beforeSelfClosing !== 'allow' && node.selfClosing) | ||
if (options.beforeSelfClosing !== "allow" && node.selfClosing) | ||
validateBeforeSelfClosing(context, node, options.beforeSelfClosing); | ||
if (options.beforeClosing !== 'allow') | ||
if (options.beforeClosing !== "allow") | ||
validateBeforeClosing(context, node, options.beforeClosing); | ||
}, | ||
JSXClosingElement(node) { | ||
if (options.afterOpening !== 'allow') | ||
if (options.afterOpening !== "allow") | ||
validateAfterOpening(context, node, options.afterOpening); | ||
if (options.closingSlash !== 'allow') | ||
if (options.closingSlash !== "allow") | ||
validateClosingSlash(context, node, options.closingSlash); | ||
if (options.beforeClosing !== 'allow') | ||
if (options.beforeClosing !== "allow") | ||
validateBeforeClosing(context, node, options.beforeClosing); | ||
}, | ||
} | ||
}, | ||
} | ||
}; | ||
} | ||
}; | ||
exports.jsxTagSpacing = jsxTagSpacing; |
@@ -5,75 +5,54 @@ 'use strict'; | ||
/** | ||
* @fileoverview Prevent missing parentheses around multilines JSX | ||
* @author Yannick Croissant | ||
*/ | ||
const has = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); | ||
// ------------------------------------------------------------------------------ | ||
// Constants | ||
// ------------------------------------------------------------------------------ | ||
const DEFAULTS = { | ||
declaration: 'parens', | ||
assignment: 'parens', | ||
return: 'parens', | ||
arrow: 'parens', | ||
condition: 'ignore', | ||
logical: 'ignore', | ||
prop: 'ignore', | ||
declaration: "parens", | ||
assignment: "parens", | ||
return: "parens", | ||
arrow: "parens", | ||
condition: "ignore", | ||
logical: "ignore", | ||
prop: "ignore" | ||
}; | ||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
const messages = { | ||
missingParens: 'Missing parentheses around multilines JSX', | ||
parensOnNewLines: 'Parentheses around JSX should be on separate lines', | ||
missingParens: "Missing parentheses around multilines JSX", | ||
parensOnNewLines: "Parentheses around JSX should be on separate lines" | ||
}; | ||
var jsxWrapMultilines = { | ||
meta: { | ||
docs: { | ||
description: 'Disallow missing parentheses around multiline JSX', | ||
category: 'Stylistic Issues', | ||
recommended: false, | ||
url: utils.docsUrl('jsx-wrap-multilines'), | ||
description: "Disallow missing parentheses around multiline JSX", | ||
category: "Stylistic Issues", | ||
url: utils.docsUrl("jsx-wrap-multilines") | ||
}, | ||
fixable: 'code', | ||
fixable: "code", | ||
messages, | ||
schema: [{ | ||
type: 'object', | ||
type: "object", | ||
// true/false are for backwards compatibility | ||
properties: { | ||
declaration: { | ||
enum: [true, false, 'ignore', 'parens', 'parens-new-line'], | ||
enum: [true, false, "ignore", "parens", "parens-new-line"] | ||
}, | ||
assignment: { | ||
enum: [true, false, 'ignore', 'parens', 'parens-new-line'], | ||
enum: [true, false, "ignore", "parens", "parens-new-line"] | ||
}, | ||
return: { | ||
enum: [true, false, 'ignore', 'parens', 'parens-new-line'], | ||
enum: [true, false, "ignore", "parens", "parens-new-line"] | ||
}, | ||
arrow: { | ||
enum: [true, false, 'ignore', 'parens', 'parens-new-line'], | ||
enum: [true, false, "ignore", "parens", "parens-new-line"] | ||
}, | ||
condition: { | ||
enum: [true, false, 'ignore', 'parens', 'parens-new-line'], | ||
enum: [true, false, "ignore", "parens", "parens-new-line"] | ||
}, | ||
logical: { | ||
enum: [true, false, 'ignore', 'parens', 'parens-new-line'], | ||
enum: [true, false, "ignore", "parens", "parens-new-line"] | ||
}, | ||
prop: { | ||
enum: [true, false, 'ignore', 'parens', 'parens-new-line'], | ||
}, | ||
enum: [true, false, "ignore", "parens", "parens-new-line"] | ||
} | ||
}, | ||
additionalProperties: false, | ||
}], | ||
additionalProperties: false | ||
}] | ||
}, | ||
create(context) { | ||
@@ -83,65 +62,46 @@ function getOption(type) { | ||
if (has(userOptions, type)) | ||
return userOptions[type] | ||
return DEFAULTS[type] | ||
return userOptions[type]; | ||
return DEFAULTS[type]; | ||
} | ||
function isEnabled(type) { | ||
const option = getOption(type); | ||
return option && option !== 'ignore' | ||
return option && option !== "ignore"; | ||
} | ||
function needsOpeningNewLine(node) { | ||
const previousToken = context.getSourceCode().getTokenBefore(node); | ||
if (!utils.isParenthesized(context, node)) | ||
return false | ||
return false; | ||
if (previousToken.loc.end.line === node.loc.start.line) | ||
return true | ||
return false | ||
return true; | ||
return false; | ||
} | ||
function needsClosingNewLine(node) { | ||
const nextToken = context.getSourceCode().getTokenAfter(node); | ||
if (!utils.isParenthesized(context, node)) | ||
return false | ||
return false; | ||
if (node.loc.end.line === nextToken.loc.end.line) | ||
return true | ||
return false | ||
return true; | ||
return false; | ||
} | ||
function isMultilines(node) { | ||
return node.loc.start.line !== node.loc.end.line | ||
return node.loc.start.line !== node.loc.end.line; | ||
} | ||
function report(node, messageId, fix) { | ||
utils.report(context, messages[messageId], messageId, { | ||
node, | ||
fix, | ||
fix | ||
}); | ||
} | ||
function trimTokenBeforeNewline(node, tokenBefore) { | ||
// if the token before the jsx is a bracket or curly brace | ||
// we don't want a space between the opening parentheses and the multiline jsx | ||
const isBracket = tokenBefore.value === '{' || tokenBefore.value === '['; | ||
return `${tokenBefore.value.trim()}${isBracket ? '' : ' '}` | ||
const isBracket = tokenBefore.value === "{" || tokenBefore.value === "["; | ||
return `${tokenBefore.value.trim()}${isBracket ? "" : " "}`; | ||
} | ||
function check(node, type) { | ||
if (!node || !utils.isJSX(node)) | ||
return | ||
return; | ||
const sourceCode = context.getSourceCode(); | ||
const option = getOption(type); | ||
if ((option === true || option === 'parens') && !utils.isParenthesized(context, node) && isMultilines(node)) | ||
report(node, 'missingParens', fixer => fixer.replaceText(node, `(${sourceCode.getText(node)})`)); | ||
if (option === 'parens-new-line' && isMultilines(node)) { | ||
if ((option === true || option === "parens") && !utils.isParenthesized(context, node) && isMultilines(node)) | ||
report(node, "missingParens", (fixer) => fixer.replaceText(node, `(${sourceCode.getText(node)})`)); | ||
if (option === "parens-new-line" && isMultilines(node)) { | ||
if (!utils.isParenthesized(context, node)) { | ||
@@ -152,30 +112,31 @@ const tokenBefore = sourceCode.getTokenBefore(node, { includeComments: true }); | ||
if (tokenBefore.loc.end.line < start.line) { | ||
// Strip newline after operator if parens newline is specified | ||
report( | ||
node, | ||
'missingParens', | ||
fixer => fixer.replaceTextRange( | ||
[tokenBefore.range[0], tokenAfter && (tokenAfter.value === ';' || tokenAfter.value === '}') ? tokenAfter.range[0] : node.range[1]], | ||
`${trimTokenBeforeNewline(node, tokenBefore)}(\n${start.column > 0 ? ' '.repeat(start.column) : ''}${sourceCode.getText(node)}\n${start.column > 0 ? ' '.repeat(start.column - 2) : ''})`, | ||
), | ||
"missingParens", | ||
(fixer) => fixer.replaceTextRange( | ||
[tokenBefore.range[0], tokenAfter && (tokenAfter.value === ";" || tokenAfter.value === "}") ? tokenAfter.range[0] : node.range[1]], | ||
`${trimTokenBeforeNewline(node, tokenBefore)}( | ||
${start.column > 0 ? " ".repeat(start.column) : ""}${sourceCode.getText(node)} | ||
${start.column > 0 ? " ".repeat(start.column - 2) : ""})` | ||
) | ||
); | ||
} else { | ||
report(node, "missingParens", (fixer) => fixer.replaceText(node, `( | ||
${sourceCode.getText(node)} | ||
)`)); | ||
} | ||
else { | ||
report(node, 'missingParens', fixer => fixer.replaceText(node, `(\n${sourceCode.getText(node)}\n)`)); | ||
} | ||
} | ||
else { | ||
} else { | ||
const needsOpening = needsOpeningNewLine(node); | ||
const needsClosing = needsClosingNewLine(node); | ||
if (needsOpening || needsClosing) { | ||
report(node, 'parensOnNewLines', (fixer) => { | ||
report(node, "parensOnNewLines", (fixer) => { | ||
const text = sourceCode.getText(node); | ||
let fixed = text; | ||
if (needsOpening) | ||
fixed = `\n${fixed}`; | ||
fixed = ` | ||
${fixed}`; | ||
if (needsClosing) | ||
fixed = `${fixed}\n`; | ||
return fixer.replaceText(node, fixed) | ||
fixed = `${fixed} | ||
`; | ||
return fixer.replaceText(node, fixed); | ||
}); | ||
@@ -186,51 +147,38 @@ } | ||
} | ||
// -------------------------------------------------------------------------- | ||
// Public | ||
// -------------------------------------------------------------------------- | ||
return { | ||
VariableDeclarator(node) { | ||
const type = 'declaration'; | ||
const type = "declaration"; | ||
if (!isEnabled(type)) | ||
return | ||
if (!isEnabled('condition') && node.init && node.init.type === 'ConditionalExpression') { | ||
return; | ||
if (!isEnabled("condition") && node.init && node.init.type === "ConditionalExpression") { | ||
check(node.init.consequent, type); | ||
check(node.init.alternate, type); | ||
return | ||
return; | ||
} | ||
check(node.init, type); | ||
}, | ||
AssignmentExpression(node) { | ||
const type = 'assignment'; | ||
const type = "assignment"; | ||
if (!isEnabled(type)) | ||
return | ||
if (!isEnabled('condition') && node.right.type === 'ConditionalExpression') { | ||
return; | ||
if (!isEnabled("condition") && node.right.type === "ConditionalExpression") { | ||
check(node.right.consequent, type); | ||
check(node.right.alternate, type); | ||
return | ||
return; | ||
} | ||
check(node.right, type); | ||
}, | ||
ReturnStatement(node) { | ||
const type = 'return'; | ||
const type = "return"; | ||
if (isEnabled(type)) | ||
check(node.argument, type); | ||
}, | ||
'ArrowFunctionExpression:exit': (node) => { | ||
"ArrowFunctionExpression:exit": (node) => { | ||
const arrowBody = node.body; | ||
const type = 'arrow'; | ||
if (isEnabled(type) && arrowBody.type !== 'BlockStatement') | ||
const type = "arrow"; | ||
if (isEnabled(type) && arrowBody.type !== "BlockStatement") | ||
check(arrowBody, type); | ||
}, | ||
ConditionalExpression(node) { | ||
const type = 'condition'; | ||
const type = "condition"; | ||
if (isEnabled(type)) { | ||
@@ -241,18 +189,16 @@ check(node.consequent, type); | ||
}, | ||
LogicalExpression(node) { | ||
const type = 'logical'; | ||
const type = "logical"; | ||
if (isEnabled(type)) | ||
check(node.right, type); | ||
}, | ||
JSXAttribute(node) { | ||
const type = 'prop'; | ||
if (isEnabled(type) && node.value && node.value.type === 'JSXExpressionContainer') | ||
const type = "prop"; | ||
if (isEnabled(type) && node.value && node.value.type === "JSXExpressionContainer") | ||
check(node.value.expression, type); | ||
}, | ||
} | ||
}, | ||
} | ||
}; | ||
} | ||
}; | ||
exports.jsxWrapMultilines = jsxWrapMultilines; |
@@ -5,8 +5,12 @@ 'use strict'; | ||
function createRule(rule) { | ||
return rule; | ||
} | ||
function docsUrl(ruleName) { | ||
return `https://eslint.style/rules/jsx/${ruleName}` | ||
return `https://eslint.style/rules/jsx/${ruleName}`; | ||
} | ||
function getMessageData(messageId) { | ||
return { messageId } | ||
return { messageId }; | ||
} | ||
@@ -18,81 +22,33 @@ | ||
getMessageData(messageId), | ||
data, | ||
), | ||
data | ||
) | ||
); | ||
} | ||
/** | ||
* @fileoverview Utility functions for AST | ||
*/ | ||
/** | ||
* Wrapper for estraverse.traverse | ||
* | ||
* @param {ASTNode} ASTnode The AST node being checked | ||
* @param {object} visitor Visitor Object for estraverse | ||
*/ | ||
function traverse(ASTnode, visitor) { | ||
const opts = Object.assign({}, { | ||
fallback(node) { | ||
return Object.keys(node).filter(key => key === 'children' || key === 'argument') | ||
}, | ||
return Object.keys(node).filter((key) => key === "children" || key === "argument"); | ||
} | ||
}, visitor); | ||
opts.keys = Object.assign({}, visitor.keys, { | ||
JSXElement: ['children'], | ||
JSXFragment: ['children'], | ||
JSXElement: ["children"], | ||
JSXFragment: ["children"] | ||
}); | ||
estraverse.traverse(ASTnode, opts); | ||
} | ||
/** | ||
* Helper function for traversing "returns" (return statements or the | ||
* returned expression in the case of an arrow function) of a function | ||
* | ||
* @param {ASTNode} ASTNode The AST node being checked | ||
* @param {Context} context The context of `ASTNode`. | ||
* @param {(returnValue: ASTNode, breakTraverse: () => void) => void} onReturn | ||
* Function to execute for each returnStatement found | ||
* @returns {undefined} | ||
*/ | ||
function traverseReturns(ASTNode, context, onReturn) { | ||
const nodeType = ASTNode.type; | ||
if (nodeType === 'ReturnStatement') { | ||
onReturn(ASTNode.argument, () => {}); | ||
return | ||
if (nodeType === "ReturnStatement") { | ||
onReturn(ASTNode.argument, () => { | ||
}); | ||
return; | ||
} | ||
if (nodeType === 'ArrowFunctionExpression' && ASTNode.expression) { | ||
onReturn(ASTNode.body, () => {}); | ||
return | ||
if (nodeType === "ArrowFunctionExpression" && ASTNode.expression) { | ||
onReturn(ASTNode.body, () => { | ||
}); | ||
return; | ||
} | ||
/* TODO: properly warn on React.forwardRefs having typo properties | ||
if (nodeType === 'CallExpression') { | ||
const callee = ASTNode.callee; | ||
const pragma = pragmaUtil.getFromContext(context); | ||
if ( | ||
callee.type === 'MemberExpression' | ||
&& callee.object.type === 'Identifier' | ||
&& callee.object.name === pragma | ||
&& callee.property.type === 'Identifier' | ||
&& callee.property.name === 'forwardRef' | ||
&& ASTNode.arguments.length > 0 | ||
) { | ||
return enterFunc(ASTNode.arguments[0]); | ||
} | ||
if (nodeType !== "FunctionExpression" && nodeType !== "FunctionDeclaration" && nodeType !== "ArrowFunctionExpression" && nodeType !== "MethodDefinition") | ||
return; | ||
} | ||
*/ | ||
if ( | ||
nodeType !== 'FunctionExpression' | ||
&& nodeType !== 'FunctionDeclaration' | ||
&& nodeType !== 'ArrowFunctionExpression' | ||
&& nodeType !== 'MethodDefinition' | ||
) | ||
return | ||
traverse(ASTNode.body, { | ||
@@ -104,26 +60,19 @@ enter(node) { | ||
switch (node.type) { | ||
case 'ReturnStatement': | ||
case "ReturnStatement": | ||
this.skip(); | ||
onReturn(node.argument, breakTraverse); | ||
return | ||
case 'BlockStatement': | ||
case 'IfStatement': | ||
case 'ForStatement': | ||
case 'WhileStatement': | ||
case 'SwitchStatement': | ||
case 'SwitchCase': | ||
return | ||
return; | ||
case "BlockStatement": | ||
case "IfStatement": | ||
case "ForStatement": | ||
case "WhileStatement": | ||
case "SwitchStatement": | ||
case "SwitchCase": | ||
return; | ||
default: | ||
this.skip(); | ||
} | ||
}, | ||
} | ||
}); | ||
} | ||
/** | ||
* Gets the first node in a line from the initial node, excluding whitespace. | ||
* @param {object} context The node to check | ||
* @param {ASTNode} node The node to check | ||
* @return {ASTNode} the first node in the line | ||
*/ | ||
function getFirstNodeInLine(context, node) { | ||
@@ -135,18 +84,6 @@ const sourceCode = context.getSourceCode(); | ||
token = sourceCode.getTokenBefore(token); | ||
lines = token.type === 'JSXText' | ||
? token.value.split('\n') | ||
: null; | ||
} while ( | ||
token.type === 'JSXText' | ||
&& /^\s*$/.test(lines[lines.length - 1]) | ||
) | ||
return token | ||
lines = token.type === "JSXText" ? token.value.split("\n") : null; | ||
} while (token.type === "JSXText" && /^\s*$/.test(lines[lines.length - 1])); | ||
return token; | ||
} | ||
/** | ||
* Checks if the node is the first in its line, excluding whitespace. | ||
* @param {object} context The node to check | ||
* @param {ASTNode} node The node to check | ||
* @return {boolean} true if it's the first node in its line | ||
*/ | ||
function isNodeFirstInLine(context, node) { | ||
@@ -156,12 +93,4 @@ const token = getFirstNodeInLine(context, node); | ||
const endLine = token ? token.loc.end.line : -1; | ||
return startLine !== endLine | ||
return startLine !== endLine; | ||
} | ||
/** | ||
* Checks if a node is surrounded by parenthesis. | ||
* | ||
* @param {object} context - Context from the rule | ||
* @param {ASTNode} node - Node to be checked | ||
* @returns {boolean} | ||
*/ | ||
function isParenthesized(context, node) { | ||
@@ -171,70 +100,29 @@ const sourceCode = context.getSourceCode(); | ||
const nextToken = sourceCode.getTokenAfter(node); | ||
return !!previousToken && !!nextToken | ||
&& previousToken.value === '(' && previousToken.range[1] <= node.range[0] | ||
&& nextToken.value === ')' && nextToken.range[0] >= node.range[1] | ||
return !!previousToken && !!nextToken && previousToken.value === "(" && previousToken.range[1] <= node.range[0] && nextToken.value === ")" && nextToken.range[0] >= node.range[1]; | ||
} | ||
/** | ||
* @fileoverview Utility functions for React pragma configuration | ||
* @author Yannick Croissant | ||
*/ | ||
const JSX_ANNOTATION_REGEX = /@jsx\s+([^\s]+)/; | ||
// Does not check for reserved keywords or unicode characters | ||
const JS_IDENTIFIER_REGEX = /^[_$a-zA-Z][_$a-zA-Z0-9]*$/; | ||
/** | ||
* @param {Context} context | ||
* @returns {string} | ||
*/ | ||
function getFromContext(context) { | ||
let pragma = 'React'; | ||
let pragma = "React"; | ||
const sourceCode = context.getSourceCode(); | ||
const pragmaNode = sourceCode.getAllComments().find(node => JSX_ANNOTATION_REGEX.test(node.value)); | ||
const pragmaNode = sourceCode.getAllComments().find((node) => JSX_ANNOTATION_REGEX.test(node.value)); | ||
if (pragmaNode) { | ||
const matches = JSX_ANNOTATION_REGEX.exec(pragmaNode.value); | ||
pragma = matches[1].split('.')[0]; | ||
// .eslintrc shared settings (https://eslint.org/docs/user-guide/configuring#adding-shared-settings) | ||
} | ||
else if (context.settings.react && context.settings.react.pragma) { | ||
pragma = matches[1].split(".")[0]; | ||
} else if (context.settings.react && context.settings.react.pragma) { | ||
pragma = context.settings.react.pragma; | ||
} | ||
if (!JS_IDENTIFIER_REGEX.test(pragma)) | ||
throw new Error(`React pragma ${pragma} is not a valid identifier`) | ||
return pragma | ||
throw new Error(`React pragma ${pragma} is not a valid identifier`); | ||
return pragma; | ||
} | ||
/** | ||
* @fileoverview Utility functions for React components detection | ||
* @author Yannick Croissant | ||
*/ | ||
/** | ||
* Find and return a particular variable in a list | ||
* @param {Array} variables The variables list. | ||
* @param {string} name The name of the variable to search. | ||
* @returns {object} Variable if the variable was found, null if not. | ||
*/ | ||
function getVariable(variables, name) { | ||
return variables.find(variable => variable.name === name) | ||
return variables.find((variable) => variable.name === name); | ||
} | ||
/** | ||
* List all variable in a given scope | ||
* | ||
* Contain a patch for babel-eslint to avoid https://github.com/babel/babel-eslint/issues/21 | ||
* | ||
* @param {object} context The current rule context. | ||
* @returns {Array} The variables list | ||
*/ | ||
function variablesInScope(context) { | ||
let scope = context.getScope(); | ||
let variables = scope.variables; | ||
while (scope.type !== 'global') { | ||
while (scope.type !== "global") { | ||
scope = scope.upper; | ||
@@ -249,43 +137,18 @@ variables = scope.variables.concat(variables); | ||
variables.reverse(); | ||
return variables | ||
return variables; | ||
} | ||
/** | ||
* Find a variable by name in the current scope. | ||
* @param {object} context The current rule context. | ||
* @param {string} name Name of the variable to look for. | ||
* @returns {ASTNode|null} Return null if the variable could not be found, ASTNode otherwise. | ||
*/ | ||
function findVariableByName(context, name) { | ||
const variable = getVariable(variablesInScope(context), name); | ||
if (!variable || !variable.defs[0] || !variable.defs[0].node) | ||
return null | ||
if (variable.defs[0].node.type === 'TypeAlias') | ||
return variable.defs[0].node.right | ||
if (variable.defs[0].type === 'ImportBinding') | ||
return variable.defs[0].node | ||
return variable.defs[0].node.init | ||
return null; | ||
if (variable.defs[0].node.type === "TypeAlias") | ||
return variable.defs[0].node.right; | ||
if (variable.defs[0].type === "ImportBinding") | ||
return variable.defs[0].node; | ||
return variable.defs[0].node.init; | ||
} | ||
/** | ||
* Returns the latest definition of the variable. | ||
* @param {object} variable | ||
* @returns {object | undefined} The latest variable definition or undefined. | ||
*/ | ||
function getLatestVariableDefinition(variable) { | ||
return variable.defs[variable.defs.length - 1] | ||
return variable.defs[variable.defs.length - 1]; | ||
} | ||
/** | ||
* Check if variable is destructured from pragma import | ||
* | ||
* @param {string} variable The variable name to check | ||
* @param {Context} context eslint context | ||
* @returns {boolean} True if createElement is destructured from the pragma | ||
*/ | ||
function isDestructuredFromPragmaImport(variable, context) { | ||
@@ -298,168 +161,74 @@ const pragma = getFromContext(context); | ||
if (latestDef) { | ||
// check if latest definition is a variable declaration: 'variable = value' | ||
if (latestDef.node.type === 'VariableDeclarator' && latestDef.node.init) { | ||
// check for: 'variable = pragma.variable' | ||
if ( | ||
latestDef.node.init.type === 'MemberExpression' | ||
&& latestDef.node.init.object.type === 'Identifier' | ||
&& latestDef.node.init.object.name === pragma | ||
) | ||
return true | ||
// check for: '{variable} = pragma' | ||
if ( | ||
latestDef.node.init.type === 'Identifier' | ||
&& latestDef.node.init.name === pragma | ||
) | ||
return true | ||
// "require('react')" | ||
if (latestDef.node.type === "VariableDeclarator" && latestDef.node.init) { | ||
if (latestDef.node.init.type === "MemberExpression" && latestDef.node.init.object.type === "Identifier" && latestDef.node.init.object.name === pragma) | ||
return true; | ||
if (latestDef.node.init.type === "Identifier" && latestDef.node.init.name === pragma) | ||
return true; | ||
let requireExpression = null; | ||
// get "require('react')" from: "{variable} = require('react')" | ||
if (latestDef.node.init.type === 'CallExpression') | ||
if (latestDef.node.init.type === "CallExpression") | ||
requireExpression = latestDef.node.init; | ||
// get "require('react')" from: "variable = require('react').variable" | ||
if ( | ||
!requireExpression | ||
&& latestDef.node.init.type === 'MemberExpression' | ||
&& latestDef.node.init.object.type === 'CallExpression' | ||
) | ||
if (!requireExpression && latestDef.node.init.type === "MemberExpression" && latestDef.node.init.object.type === "CallExpression") | ||
requireExpression = latestDef.node.init.object; | ||
// check proper require. | ||
if ( | ||
requireExpression | ||
&& requireExpression.callee | ||
&& requireExpression.callee.name === 'require' | ||
&& requireExpression.arguments[0] | ||
&& requireExpression.arguments[0].value === pragma.toLocaleLowerCase() | ||
) | ||
return true | ||
return false | ||
if (requireExpression && requireExpression.callee && requireExpression.callee.name === "require" && requireExpression.arguments[0] && requireExpression.arguments[0].value === pragma.toLocaleLowerCase()) | ||
return true; | ||
return false; | ||
} | ||
// latest definition is an import declaration: import {<variable>} from 'react' | ||
if ( | ||
latestDef.parent | ||
&& latestDef.parent.type === 'ImportDeclaration' | ||
&& latestDef.parent.source.value === pragma.toLocaleLowerCase() | ||
) | ||
return true | ||
if (latestDef.parent && latestDef.parent.type === "ImportDeclaration" && latestDef.parent.source.value === pragma.toLocaleLowerCase()) | ||
return true; | ||
} | ||
} | ||
return false | ||
return false; | ||
} | ||
/** | ||
* Checks if the node is a createElement call | ||
* @param {ASTNode} node - The AST node being checked. | ||
* @param {Context} context - The AST node being checked. | ||
* @returns {boolean} - True if node is a createElement call object literal, False if not. | ||
*/ | ||
function isCreateElement(node, context) { | ||
if ( | ||
node.callee | ||
&& node.callee.type === 'MemberExpression' | ||
&& node.callee.property.name === 'createElement' | ||
&& node.callee.object | ||
&& node.callee.object.name === getFromContext(context) | ||
) | ||
return true | ||
if ( | ||
node | ||
&& node.callee | ||
&& node.callee.name === 'createElement' | ||
&& isDestructuredFromPragmaImport('createElement', context) | ||
) | ||
return true | ||
return false | ||
if (node.callee && node.callee.type === "MemberExpression" && node.callee.property.name === "createElement" && node.callee.object && node.callee.object.name === getFromContext(context)) | ||
return true; | ||
if (node && node.callee && node.callee.name === "createElement" && isDestructuredFromPragmaImport("createElement", context)) | ||
return true; | ||
return false; | ||
} | ||
/** | ||
* @fileoverview Utility functions for JSX | ||
*/ | ||
// See https://github.com/babel/babel/blob/ce420ba51c68591e057696ef43e028f41c6e04cd/packages/babel-types/src/validators/react/isCompatTag.js | ||
// for why we only test for the first character | ||
const COMPAT_TAG_REGEX = /^[a-z]/; | ||
/** | ||
* Checks if a node represents a DOM element according to React. | ||
* @param {object} node - JSXOpeningElement to check. | ||
* @returns {boolean} Whether or not the node corresponds to a DOM element. | ||
*/ | ||
function isDOMComponent(node) { | ||
const name = getElementType(node); | ||
return COMPAT_TAG_REGEX.test(name) | ||
return COMPAT_TAG_REGEX.test(name); | ||
} | ||
/** | ||
* Checks if a node represents a JSX element or fragment. | ||
* @param {object} node - node to check. | ||
* @returns {boolean} Whether or not the node if a JSX element or fragment. | ||
*/ | ||
function isJSX(node) { | ||
return node && ['JSXElement', 'JSXFragment'].includes(node.type) | ||
return node && ["JSXElement", "JSXFragment"].includes(node.type); | ||
} | ||
/** | ||
* Check if value has only whitespaces | ||
* @param {string} value | ||
* @returns {boolean} | ||
*/ | ||
function isWhiteSpaces(value) { | ||
return typeof value === 'string' ? /^\s*$/.test(value) : false | ||
return typeof value === "string" ? /^\s*$/.test(value) : false; | ||
} | ||
/** | ||
* Check if the node is returning JSX or null | ||
* | ||
* @param {ASTNode} ASTnode The AST node being checked | ||
* @param {Context} context The context of `ASTNode`. | ||
* @param {boolean} [strict] If true, in a ternary condition the node must return JSX in both cases | ||
* @param {boolean} [ignoreNull] If true, null return values will be ignored | ||
* @returns {boolean} True if the node is returning JSX or null, false if not | ||
*/ | ||
function isReturningJSX(ASTnode, context, strict, ignoreNull) { | ||
const isJSXValue = (node) => { | ||
if (!node) | ||
return false | ||
return false; | ||
switch (node.type) { | ||
case 'ConditionalExpression': | ||
case "ConditionalExpression": | ||
if (strict) | ||
return isJSXValue(node.consequent) && isJSXValue(node.alternate) | ||
return isJSXValue(node.consequent) || isJSXValue(node.alternate) | ||
case 'LogicalExpression': | ||
return isJSXValue(node.consequent) && isJSXValue(node.alternate); | ||
return isJSXValue(node.consequent) || isJSXValue(node.alternate); | ||
case "LogicalExpression": | ||
if (strict) | ||
return isJSXValue(node.left) && isJSXValue(node.right) | ||
return isJSXValue(node.left) || isJSXValue(node.right) | ||
case 'SequenceExpression': | ||
return isJSXValue(node.expressions[node.expressions.length - 1]) | ||
case 'JSXElement': | ||
case 'JSXFragment': | ||
return true | ||
case 'CallExpression': | ||
return isCreateElement(node, context) | ||
case 'Literal': | ||
return isJSXValue(node.left) && isJSXValue(node.right); | ||
return isJSXValue(node.left) || isJSXValue(node.right); | ||
case "SequenceExpression": | ||
return isJSXValue(node.expressions[node.expressions.length - 1]); | ||
case "JSXElement": | ||
case "JSXFragment": | ||
return true; | ||
case "CallExpression": | ||
return isCreateElement(node, context); | ||
case "Literal": | ||
if (!ignoreNull && node.value === null) | ||
return true | ||
return false | ||
case 'Identifier': { | ||
return true; | ||
return false; | ||
case "Identifier": { | ||
const variable = findVariableByName(context, node.name); | ||
return isJSX(variable) | ||
return isJSX(variable); | ||
} | ||
default: | ||
return false | ||
return false; | ||
} | ||
}; | ||
let found = false; | ||
@@ -472,66 +241,39 @@ traverseReturns(ASTnode, context, (node, breakTraverse) => { | ||
}); | ||
return found | ||
return found; | ||
} | ||
/** | ||
* Returns the name of the prop given the JSXAttribute object. | ||
* | ||
* Ported from `jsx-ast-utils/propName` to reduce bundle size | ||
* @see https://github.com/jsx-eslint/jsx-ast-utils/blob/main/src/propName.js | ||
*/ | ||
function getPropName(prop = {}) { | ||
if (!prop.type || prop.type !== 'JSXAttribute') | ||
throw new Error('The prop must be a JSXAttribute collected by the AST parser.') | ||
if (prop.name.type === 'JSXNamespacedName') | ||
return `${prop.name.namespace.name}:${prop.name.name.name}` | ||
return prop.name.name | ||
if (!prop.type || prop.type !== "JSXAttribute") | ||
throw new Error("The prop must be a JSXAttribute collected by the AST parser."); | ||
if (prop.name.type === "JSXNamespacedName") | ||
return `${prop.name.namespace.name}:${prop.name.name.name}`; | ||
return prop.name.name; | ||
} | ||
function resolveMemberExpressions(object = {}, property = {}) { | ||
if (object.type === 'JSXMemberExpression') | ||
return `${resolveMemberExpressions(object.object, object.property)}.${property.name}` | ||
return `${object.name}.${property.name}` | ||
if (object.type === "JSXMemberExpression") | ||
return `${resolveMemberExpressions(object.object, object.property)}.${property.name}`; | ||
return `${object.name}.${property.name}`; | ||
} | ||
/** | ||
* Returns the tagName associated with a JSXElement. | ||
* | ||
* Ported from `jsx-ast-utils/elementType` to reduce bundle size | ||
* @see https://github.com/jsx-eslint/jsx-ast-utils/blob/main/src/elementType.js | ||
*/ | ||
function getElementType(node = {}) { | ||
const { name } = node; | ||
if (node.type === 'JSXOpeningFragment') | ||
return '<>' | ||
if (node.type === "JSXOpeningFragment") | ||
return "<>"; | ||
if (!name) | ||
throw new Error('The argument provided is not a JSXElement node.') | ||
if (name.type === 'JSXMemberExpression') { | ||
throw new Error("The argument provided is not a JSXElement node."); | ||
if (name.type === "JSXMemberExpression") { | ||
const { object = {}, property = {} } = name; | ||
return resolveMemberExpressions(object, property) | ||
return resolveMemberExpressions(object, property); | ||
} | ||
if (name.type === 'JSXNamespacedName') | ||
return `${name.namespace.name}:${name.name.name}` | ||
return node.name.name | ||
if (name.type === "JSXNamespacedName") | ||
return `${name.namespace.name}:${name.name.name}`; | ||
return node.name.name; | ||
} | ||
/** | ||
* 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) { | ||
const attributes = node.attributes; | ||
if (!attributes || attributes.length === 0) | ||
return node.name | ||
return attributes[attributes.length - 1] | ||
return node.name; | ||
return attributes[attributes.length - 1]; | ||
} | ||
exports.createRule = createRule; | ||
exports.docsUrl = docsUrl; | ||
@@ -538,0 +280,0 @@ exports.getFirstNodeInLine = getFirstNodeInLine; |
@@ -1,5 +0,2 @@ | ||
/** | ||
* This file is GENERATED by scripts/prepare.ts | ||
* DO NOT EDIT THIS FILE DIRECTLY | ||
*/ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
@@ -6,0 +3,0 @@ import type { RuleOptions } from './rule-options' |
@@ -1,2 +0,2 @@ | ||
import type { ESLint } from 'eslint' | ||
import type { ESLint, Linter } from 'eslint' | ||
import type { UnprefixedRuleOptions } from './rule-options' | ||
@@ -6,5 +6,10 @@ | ||
export type Rules = { | ||
[K in keyof UnprefixedRuleOptions]: ESLint.RuleModule | ||
} | ||
declare const plugin: { | ||
rules: { | ||
[K in keyof UnprefixedRuleOptions]: ESLint.RuleModule | ||
rules: Rules | ||
configs: { | ||
'disable-legacy': Linter.FlatConfig | ||
} | ||
@@ -11,0 +16,0 @@ } |
@@ -1,5 +0,2 @@ | ||
/** | ||
* This file is GENERATED by scripts/prepare.ts | ||
* DO NOT EDIT THIS FILE DIRECTLY | ||
*/ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
@@ -6,0 +3,0 @@ import type { RuleOptions as JsxChildElementSpacingRuleOptions } from '../rules/jsx-child-element-spacing/types' |
{ | ||
"name": "@stylistic/eslint-plugin-jsx", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"author": "Anthony Fu <anthonyfu117@hotmail.com>", | ||
@@ -55,3 +55,3 @@ "license": "MIT", | ||
"estraverse": "^5.3.0", | ||
"@stylistic/eslint-plugin-js": "^1.1.0" | ||
"@stylistic/eslint-plugin-js": "^1.2.0" | ||
}, | ||
@@ -58,0 +58,0 @@ "devDependencies": { |
@@ -0,1 +1,4 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export type RuleOptions = [] | ||
export type MessageIds = 'spacingAfterPrev' | 'spacingBeforeNext' |
@@ -0,1 +1,3 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export type Schema0 = | ||
@@ -12,1 +14,2 @@ | ('after-props' | 'props-aligned' | 'tag-aligned' | 'line-aligned') | ||
export type RuleOptions = [Schema0?] | ||
export type MessageIds = 'bracketLocation' |
@@ -0,1 +1,4 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export type RuleOptions = [] | ||
export type MessageIds = 'onOwnLine' | 'matchIndent' |
@@ -0,1 +1,3 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export type Schema0 = | ||
@@ -10,1 +12,2 @@ | { | ||
export type RuleOptions = [Schema0?] | ||
export type MessageIds = 'unnecessaryCurly' | 'missingCurly' |
@@ -0,1 +1,3 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export type Schema0 = | ||
@@ -9,1 +11,2 @@ | ('consistent' | 'never') | ||
export type RuleOptions = [Schema0?] | ||
export type MessageIds = 'expectedBefore' | 'expectedAfter' | 'unexpectedBefore' | 'unexpectedAfter' |
@@ -0,1 +1,3 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export type Schema0 = | ||
@@ -41,1 +43,2 @@ | [] | ||
export type RuleOptions = Schema0 | ||
export type MessageIds = 'noNewlineAfter' | 'noNewlineBefore' | 'noSpaceAfter' | 'noSpaceBefore' | 'spaceNeededAfter' | 'spaceNeededBefore' |
@@ -0,3 +1,6 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export type Schema0 = 'always' | 'never' | ||
export type RuleOptions = [Schema0?] | ||
export type MessageIds = 'noSpaceBefore' | 'noSpaceAfter' | 'needSpaceBefore' | 'needSpaceAfter' |
@@ -0,3 +1,6 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export type Schema0 = 'always' | 'never' | 'multiline' | 'multiline-multiprop' | 'multiprop' | ||
export type RuleOptions = [Schema0?] | ||
export type MessageIds = 'propOnNewLine' | 'propOnSameLine' |
@@ -0,1 +1,3 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export type Schema0 = | ||
@@ -11,1 +13,2 @@ | ('tab' | 'first') | ||
export type RuleOptions = [Schema0?] | ||
export type MessageIds = 'wrongIndent' |
@@ -0,1 +1,3 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export type Schema0 = 'tab' | number | ||
@@ -9,1 +11,2 @@ | ||
export type RuleOptions = [Schema0?, Schema1?] | ||
export type MessageIds = 'wrongIndent' |
@@ -0,1 +1,3 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export type Schema0 = | ||
@@ -15,1 +17,2 @@ | { | ||
export type RuleOptions = [Schema0?] | ||
export type MessageIds = 'newLine' |
@@ -0,1 +1,3 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export interface Schema0 { | ||
@@ -7,1 +9,2 @@ prevent?: boolean | ||
export type RuleOptions = [Schema0?] | ||
export type MessageIds = 'require' | 'prevent' | 'allowMultilines' |
@@ -0,1 +1,3 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export interface Schema0 { | ||
@@ -6,1 +8,2 @@ allow?: 'none' | 'literal' | 'single-child' | ||
export type RuleOptions = [Schema0?] | ||
export type MessageIds = 'moveToNewLine' |
@@ -0,1 +1,4 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export type RuleOptions = [] | ||
export type MessageIds = 'noLineGap' | 'onlyOneSpace' |
@@ -0,1 +1,3 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export interface Schema0 { | ||
@@ -7,1 +9,2 @@ component?: boolean | ||
export type RuleOptions = [Schema0?] | ||
export type MessageIds = 'notSelfClosing' |
@@ -0,1 +1,3 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export interface Schema0 { | ||
@@ -13,1 +15,2 @@ callbacksLast?: boolean | ||
export type RuleOptions = [Schema0?] | ||
export type MessageIds = 'noUnreservedProps' | 'listIsEmpty' | 'listReservedPropsFirst' | 'listCallbacksLast' | 'listShorthandFirst' | 'listShorthandLast' | 'listMultilineFirst' | 'listMultilineLast' | 'sortPropsByAlpha' |
@@ -0,1 +1,3 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export interface Schema0 { | ||
@@ -9,1 +11,2 @@ closingSlash?: 'always' | 'never' | 'allow' | ||
export type RuleOptions = [Schema0?] | ||
export type MessageIds = 'selfCloseSlashNoSpace' | 'selfCloseSlashNeedSpace' | 'closeSlashNoSpace' | 'closeSlashNeedSpace' | 'beforeSelfCloseNoSpace' | 'beforeSelfCloseNeedSpace' | 'beforeSelfCloseNeedNewline' | 'afterOpenNoSpace' | 'afterOpenNeedSpace' | 'beforeCloseNoSpace' | 'beforeCloseNeedSpace' | 'beforeCloseNeedNewline' |
@@ -0,1 +1,3 @@ | ||
/* GENERATED, DO NOT EDIT DIRECTLY */ | ||
export interface Schema0 { | ||
@@ -12,1 +14,2 @@ declaration?: true | false | 'ignore' | 'parens' | 'parens-new-line' | ||
export type RuleOptions = [Schema0?] | ||
export type MessageIds = 'missingParens' | 'parensOnNewLines' |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
81
254586
3652