diff --git a/src/configs/recommended.json b/src/configs/recommended.json index 52bac2d..a17882c 100644 --- a/src/configs/recommended.json +++ b/src/configs/recommended.json @@ -56,4 +56,4 @@ "onlyFilesWithFlowAnnotation": false } } -} +} \ No newline at end of file diff --git a/src/rules/genericSpacing.js b/src/rules/genericSpacing.js index aa9004b..3505250 100644 --- a/src/rules/genericSpacing.js +++ b/src/rules/genericSpacing.js @@ -9,12 +9,209 @@ const schema = [ }, ]; +function isNeverOption(context) { + return (context.options[0] || 'never') === 'never'; +} + +function isWhitespaceCRLF(whitespace) { + return whitespace !== '\n' && whitespace !== '\r'; +} + +function spacesOutside(node, context) { + const { callee, typeArguments } = node; + if (typeArguments == null) { + return; + } + + const sourceCode = context.getSourceCode(); + const { name } = callee; + const never = isNeverOption(context); + const parentheses = sourceCode.getTokenAfter(typeArguments); + + const spacesBefore = typeArguments.range[0] - callee.range[1]; + const spacesAfter = parentheses.range[0] - typeArguments.range[1]; + + if (never) { + if (spacesBefore) { + const whiteSpaceBefore = sourceCode.text[typeArguments.range[0]]; + + if (isWhitespaceCRLF(whiteSpaceBefore)) { + context.report({ + data: { name }, + fix: spacingFixers.stripSpacesBefore(typeArguments, spacesBefore), + message: 'There must be no space before "{{name}}" type annotation', + node, + }); + } + } + + if (spacesAfter) { + const whiteSpaceAfter = sourceCode.text[typeArguments.range[1] - 1]; + + if (isWhitespaceCRLF(whiteSpaceAfter)) { + context.report({ + data: { name }, + fix: spacingFixers.stripSpacesAfter(typeArguments, spacesAfter), + message: 'There must be no space after "{{name}}" type annotation', + node, + }); + } + } + + return; + } + + if (!never) { + if (spacesBefore > 1) { + context.report({ + data: { name }, + fix: spacingFixers.stripSpacesBefore(typeArguments, spacesBefore - 1), + message: 'There must be one space before "{{name}}" generic type annotation bracket', + node, + }); + } + + if (spacesBefore === 0) { + context.report({ + data: { name }, + fix: spacingFixers.addSpaceBefore(typeArguments), + message: 'There must be a space before "{{name}}" generic type annotation bracket', + node, + }); + } + + if (spacesAfter > 1) { + context.report({ + data: { name }, + fix: spacingFixers.stripSpacesAfter(typeArguments, spacesAfter), + message: 'There must be one space before "{{name}}" generic type annotation bracket', + node, + }); + } + + if (spacesAfter === 0) { + context.report({ + data: { name }, + fix: spacingFixers.addSpaceAfter(typeArguments), + message: 'There must be a space before "{{name}}" generic type annotation bracket', + node, + }); + } + } +} + +function spacesInside(node, context) { + const { callee, typeArguments } = node; + if (typeArguments == null) { + return; + } + + const sourceCode = context.getSourceCode(); + const { name } = callee; + const never = isNeverOption(context); + const isNullable = typeArguments.params[0].type === 'NullableTypeAnnotation'; + const [ + opener, + firstInnerToken, + secondInnerToken, + ] = sourceCode.getFirstTokens(typeArguments, 3); + const [ + lastInnerToken, + closer, + ] = sourceCode.getLastTokens(typeArguments, 2); + + const spacesBefore = firstInnerToken.range[0] - opener.range[1]; + const spaceBetweenNullToken = secondInnerToken.range[0] - firstInnerToken.range[1]; + const spacesAfter = closer.range[0] - lastInnerToken.range[1]; + + if (never) { + if (spacesBefore) { + const whiteSpaceBefore = sourceCode.text[opener.range[1]]; + + if (whiteSpaceBefore !== '\n' && whiteSpaceBefore !== '\r') { + context.report({ + data: { name }, + fix: spacingFixers.stripSpacesAfter(opener, spacesBefore), + message: 'There must be no spaces inside at the start of "{{name}}" type annotation', + node, + }); + } + } + + if (isNullable && spaceBetweenNullToken) { + context.report({ + data: { name }, + fix: spacingFixers.stripSpacesAfter(firstInnerToken, spaceBetweenNullToken), + message: 'There must be no spaces inside "{{name}}" type annotation', + node, + }); + } + + if (spacesAfter) { + const whiteSpaceAfter = sourceCode.text[closer.range[0] - 1]; + + if (isWhitespaceCRLF(whiteSpaceAfter)) { + context.report({ + data: { name }, + fix: spacingFixers.stripSpacesAfter(lastInnerToken, spacesAfter), + message: 'There must be no spaces inside at the end of "{{name}}" type annotation', + node, + }); + } + } + + return; + } + + if (!never) { + if (spacesBefore > 1) { + context.report({ + data: { name }, + fix: spacingFixers.stripSpacesBefore(opener, spacesBefore - 1), + message: 'There must be one space before "{{name}}" generic type annotation bracket', + node, + }); + } + + if (spacesBefore === 0) { + context.report({ + data: { name }, + fix: spacingFixers.addSpaceBefore(opener), + message: 'There must be a space before "{{name}}" generic type annotation bracket', + node, + }); + } + + if (spacesAfter > 1) { + context.report({ + data: { name }, + fix: spacingFixers.stripSpacesAfter(closer, spacesAfter), + message: 'There must be one space before "{{name}}" generic type annotation bracket', + node, + }); + } + + if (spacesAfter === 0) { + context.report({ + data: { name }, + fix: spacingFixers.addSpaceAfter(closer), + message: 'There must be a space before "{{name}}" generic type annotation bracket', + node, + }); + } + } +} + const create = (context) => { const sourceCode = context.getSourceCode(); const never = (context.options[0] || 'never') === 'never'; return { + CallExpression(node) { + spacesOutside(node, context); + spacesInside(node, context); + }, GenericTypeAnnotation(node) { const types = node.typeParameters; diff --git a/tests/rules/assertions/genericSpacing.js b/tests/rules/assertions/genericSpacing.js index 8deb7f3..d4249fe 100644 --- a/tests/rules/assertions/genericSpacing.js +++ b/tests/rules/assertions/genericSpacing.js @@ -1,7 +1,6 @@ export default { invalid: [ // Never - { code: 'type X = Promise< string>', errors: [{ message: 'There must be no space at start of "Promise" generic type annotation' }], @@ -91,6 +90,68 @@ export default { options: ['always'], output: 'type X = Promise< (foo), bar, (((baz))) >', }, + + // Type annotations + { + code: 'const [state, setState] = useState(null)', + errors: [{ message: 'There must be no space at start of type annotations' }], + output: 'const [state, setState] = useState(null)', + }, + { + code: 'const [state, setState] = useState (null)', + errors: [{ message: 'There must be no space at start of type annotations' }], + output: 'const [state, setState] = useState(null)', + }, + { + code: 'const [state, setState] = useState< ?string>(null)', + errors: [{ message: 'There must be no space at start of type annotations' }], + output: 'const [state, setState] = useState(null)', + }, + { + code: 'const [state, setState] = useState < ?string>(null)', + errors: [{ message: 'There must be no space at start of type annotations' }], + output: 'const [state, setState] = useState(null)', + }, + { + code: 'const [state, setState] = useState(null)', + errors: [{ message: 'There must be no space at start of type annotations' }], + output: 'const [state, setState] = useState(null)', + }, + { + code: 'const [state, setState] = useState< ? string>(null)', + errors: [{ message: 'There must be no space at start of type annotations' }], + output: 'const [state, setState] = useState(null)', + }, + { + code: 'const [state, setState] = useState< ? string >(null)', + errors: [{ message: 'There must be no space at start of type annotations' }], + output: 'const [state, setState] = useState(null)', + }, + { + code: 'const [state, setState] = useState < ? string > (null)', + errors: [{ message: 'There must be no space at start of type annotations' }], + output: 'const [state, setState] = useState(null)', + }, + { + code: 'const [state, setState] = useState < ? string > ()', + errors: [{ message: 'There must be no space at start of type annotations' }], + output: 'const [state, setState] = useState(null)', + }, + { + code: 'useState(null)', + errors: [{ message: 'There must be no space at start of type annotations' }], + output: 'useState(null)', + }, + { + code: 'useState< string>(null)', + errors: [{ message: 'There must be no space at start of type annotations' }], + output: 'useState(null)', + }, + { + code: 'useState< string >(null)', + errors: [{ message: 'There must be no space at start of type annotations' }], + output: 'useState(null)', + }, ], misconfigured: [ { @@ -131,7 +192,7 @@ export default { { code: 'type X = Promise<(foo), bar, (((baz)))>' }, { code: -`type X = Promise< + `type X = Promise< (foo), bar, (((baz))) @@ -153,5 +214,43 @@ export default { code: 'type X = Promise< (foo), bar, (((baz))) >', options: ['always'], }, + { + code: 'const [state, setState] = useState< string >("")', + options: ['always'], + }, + { + code: 'const [state, setState] = useState< ?string >(null)', + options: ['always'], + }, + { + code: 'const [state, setState] = useState< string | null >(null)', + options: ['always'], + }, + { + code: 'const [state, setState] = useState< string | number >(2)', + options: ['always'], + }, + + // Never + { + code: 'const [state, setState] = useState(null)', + options: ['never'], + }, + { + code: 'const [state, setState] = useState("")', + options: ['never'], + }, + { + code: 'const [state, setState] = useState(null)', + options: ['never'], + }, + { + code: 'const [state, setState] = useState(null)', + options: ['never'], + }, + { + code: 'const [state, setState] = useState(2)', + options: ['never'], + }, ], };