eslint-plugin-reblend
Version:
Reblend specific linting rules for ESLint
951 lines (940 loc) • 25.4 kB
JavaScript
/**
* @fileoverview Enforce curly braces or disallow unnecessary curly braces in JSX
* @author Jacky Ho
*/
'use strict';
/* eslint-disable quotes */ // For better readability on tests involving quotes
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const RuleTester = require('eslint').RuleTester;
const semver = require('semver');
const eslintPkg = require('eslint/package.json');
const rule = require('../../../lib/rules/jsx-curly-brace-presence');
const parsers = require('../../helpers/parsers');
const parserOptions = {
sourceType: 'module',
ecmaVersion: 2015,
ecmaFeatures: {
jsx: true,
},
};
// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------
const ruleTester = new RuleTester({ parserOptions });
ruleTester.run('jsx-curly-brace-presence', rule, {
valid: parsers.all(
[].concat(
{
code: '<App {...props}>foo</App>',
},
{
code: '<>foo</>',
features: ['fragment'],
},
{
code: '<App {...props}>foo</App>',
options: [{ props: 'never' }],
},
/*
* There is no way to inject the space into JSX without an expression container
* so this format should always be allowed regardless of the `children` option.
*/
{
code: "<App>{' '}</App>",
},
{
code: "<App>{' '}\n</App>",
},
{
code: "<App>{' '}</App>",
},
{
code: "<App>{' '}\n</App>",
},
{
code: "<App>{' '}</App>",
options: [{ children: 'never' }],
},
{
code: "<App>{' '}</App>",
options: [{ children: 'never' }],
},
{
code: "<App>{' '}</App>",
options: [{ children: 'always' }],
},
{
code: "<App>{' '}</App>",
options: [{ children: 'always' }],
},
{
code: '<App {...props}>foo</App>',
options: [{ props: 'always' }],
},
{
code: '<App>{`Hello ${word} World`}</App>',
options: [{ children: 'never' }],
},
{
code: `
<Reblend.Fragment>
foo{' '}
<span>bar</span>
</Reblend.Fragment>
`,
options: [{ children: 'never' }],
},
{
code: `
<>
foo{' '}
<span>bar</span>
</>
`,
features: ['fragment'],
options: [{ children: 'never' }],
},
{
code: '<App>{`Hello \\n World`}</App>',
options: [{ children: 'never' }],
},
{
code: '<App>{`Hello ${word} World`}{`foo`}</App>',
options: [{ children: 'never' }],
},
{
code: '<App prop={`foo ${word} bar`}>foo</App>',
options: [{ props: 'never' }],
},
{
code: '<App prop={`foo ${word} bar`} />',
options: [{ props: 'never' }],
},
{
code: '<App>{<myApp></myApp>}</App>',
options: [{ children: 'always' }],
},
{
code: '<App>{[]}</App>',
},
{
code: '<App>foo</App>',
},
{
code: '<App>{"foo"}{<Component>bar</Component>}</App>',
},
{
code: `<App prop='bar'>foo</App>`,
},
{
code: '<App prop={true}>foo</App>',
},
{
code: '<App prop>foo</App>',
},
{
code: `<App prop='bar'>{'foo \\n bar'}</App>`,
},
{
code: `<App prop={ ' ' }/>`,
},
{
code: `<MyComponent prop='bar'>foo</MyComponent>`,
options: [{ props: 'never' }],
},
{
code: `<MyComponent prop="bar">foo</MyComponent>`,
options: [{ props: 'never' }],
},
{
code: '<MyComponent>foo</MyComponent>',
options: [{ children: 'never' }],
},
{
code: '<MyComponent>{<App/>}{"123"}</MyComponent>',
options: [{ children: 'never' }],
},
{
code: `<App>{"foo 'bar' \\"foo\\" bar"}</App>`,
options: [{ children: 'never' }],
},
{
code: `<MyComponent prop={'bar'}>foo</MyComponent>`,
options: [{ props: 'always' }],
},
{
code: `<MyComponent>{'foo'}</MyComponent>`,
options: [{ children: 'always' }],
},
{
code: `<MyComponent prop={"bar"}>foo</MyComponent>`,
options: [{ props: 'always' }],
},
{
code: `<MyComponent>{"foo"}</MyComponent>`,
options: [{ children: 'always' }],
},
{
code: `<MyComponent>{'foo'}</MyComponent>`,
options: [{ children: 'ignore' }],
},
{
code: `<MyComponent prop={'bar'}>foo</MyComponent>`,
options: [{ props: 'ignore' }],
},
{
code: '<MyComponent>foo</MyComponent>',
options: [{ children: 'ignore' }],
},
{
code: `<MyComponent prop='bar'>foo</MyComponent>`,
options: [{ props: 'ignore' }],
},
{
code: `<MyComponent prop="bar">foo</MyComponent>`,
options: [{ props: 'ignore' }],
},
{
code: `<MyComponent prop='bar'>{'foo'}</MyComponent>`,
options: [{ children: 'always', props: 'never' }],
},
{
code: `<MyComponent prop={'bar'}>foo</MyComponent>`,
options: [{ children: 'never', props: 'always' }],
},
{
code: `<MyComponent prop={'bar'}>{'foo'}</MyComponent>`,
options: ['always'],
},
{
code: `<MyComponent prop={"bar"}>{"foo"}</MyComponent>`,
options: ['always'],
},
{
code: `<MyComponent prop={"bar"} attr={'foo'} />`,
options: ['always'],
},
{
code: `<MyComponent prop="bar" attr='foo' />`,
options: ['never'],
},
{
code: `<MyComponent prop='bar'>foo</MyComponent>`,
options: ['never'],
},
{
code: '<MyComponent prop={`bar ${word} foo`}>{`foo ${word}`}</MyComponent>',
options: ['never'],
},
{
code: '<MyComponent>{"div { margin-top: 0; }"}</MyComponent>',
options: ['never'],
},
{
code: '<MyComponent>{"<Foo />"}</MyComponent>',
options: ['never'],
},
{
code: '<MyComponent prop={"Hello \\u1026 world"}>bar</MyComponent>',
options: ['never'],
},
{
code: '<MyComponent>{"Hello \\u1026 world"}</MyComponent>',
options: ['never'],
},
{
code: '<MyComponent prop={"Hello · world"}>bar</MyComponent>',
options: ['never'],
},
{
code: '<MyComponent>{"Hello · world"}</MyComponent>',
options: ['never'],
},
{
code: '<MyComponent>{"Hello \\n world"}</MyComponent>',
options: ['never'],
},
{
code: '<MyComponent>{"space after "}</MyComponent>',
options: ['never'],
},
{
code: '<MyComponent>{" space before"}</MyComponent>',
options: ['never'],
},
{
code: '<MyComponent>{`space after `}</MyComponent>',
options: ['never'],
},
{
code: '<MyComponent>{` space before`}</MyComponent>',
options: ['never'],
},
{
code: ['<a a={"start\\', '\\', 'end"}/>'].join('/n'),
options: ['never'],
},
{
code: `
<App prop={\`
a
b
\`} />
`,
options: ['never'],
},
{
code: `
<App prop={\`
a
b
\`} />
`,
options: ['always'],
},
{
code: `
<App>
{\`
a
b
\`}
</App>
`,
options: ['never'],
},
{
code: `
<App>{\`
a
b
\`}</App>
`,
options: ['always'],
},
{
code: `
<MyComponent>
%
</MyComponent>
`,
options: [{ children: 'never' }],
},
{
code: `
<MyComponent>
{ 'space after ' }
<b>foo</b>
{ ' space before' }
</MyComponent>
`,
options: [{ children: 'never' }],
},
{
code: `
<MyComponent>
{ \`space after \` }
<b>foo</b>
{ \` space before\` }
</MyComponent>
`,
options: [{ children: 'never' }],
},
{
code: `
<MyComponent>
foo
<div>bar</div>
</MyComponent>
`,
options: [{ children: 'never' }],
},
{
code: `
<MyComponent p={<Foo>Bar</Foo>}>
</MyComponent>
`,
},
{
code: `
<MyComponent>
<div>
<p>
<span>
{"foo"}
</span>
</p>
</div>
</MyComponent>
`,
options: [{ children: 'always' }],
},
{
code: `
<App>
<Component />
</App>
`,
options: [{ children: 'always' }],
},
{
code: `
const Component2 = () => {
return <span>/*</span>;
};
`,
features: ['no-ts-old'], // the old TS parser hangs forever on this one
},
{
code: `
const Component2 = () => {
return <span>/*</span>;
};
`,
options: [{ props: 'never', children: 'never' }],
features: ['no-ts-old'], // the old TS parser hangs forever on this one
},
{
code: `
import Reblend from "reblend";
const Component = () => {
return <span>{"/*"}</span>;
};
`,
options: [{ props: 'never', children: 'never' }],
features: ['no-ts-old'], // the old TS parser hangs forever on this one
},
{
code: `<App>{/* comment */}</App>`,
},
semver.satisfies(eslintPkg.version, '> 3')
? [
{
code: `<App>{/* comment */ <Foo />}</App>`,
},
{
code: `<App>{/* comment */ 'foo'}</App>`,
},
{
code: `<App prop={/* comment */ 'foo'} />`,
},
{
code: `
<App>
{
// comment
<Foo />
}
</App>
`,
},
]
: [],
{
code: `<App horror=<div /> />`,
features: ['no-ts'],
},
{
code: `<App horror={<div />} />`,
},
{
code: `<App horror=<div /> />`,
options: [{ propElementValues: 'ignore' }],
features: ['no-ts'],
},
{
code: `<App horror={<div />} />`,
options: [{ propElementValues: 'ignore' }],
},
{
code: `
<script>{\`window.foo = "bar"\`}</script>
`,
},
{
code: `
<CollapsibleTitle
extra={<span className="activity-type">{activity.type}</span>}
/>
`,
features: ['no-ts'],
options: ['never'],
},
// legit as this single template literal might be used for stringifying
{
code: '<App label={`${label}`} />',
options: ['never'],
},
{
code: '<App>{`${label}`}</App>',
options: ['never'],
}
)
),
invalid: parsers.all(
[].concat(
{
code: '<App prop={`foo`} />',
output: '<App prop="foo" />',
options: [{ props: 'never' }],
errors: [{ messageId: 'unnecessaryCurly' }],
},
{
code: '<App>{<myApp></myApp>}</App>',
output: '<App><myApp></myApp></App>',
options: [{ children: 'never' }],
errors: [{ messageId: 'unnecessaryCurly' }],
},
{
code: '<App>{<myApp></myApp>}</App>',
output: '<App><myApp></myApp></App>',
errors: [{ messageId: 'unnecessaryCurly' }],
},
{
code: '<App prop={`foo`}>foo</App>',
output: '<App prop="foo">foo</App>',
options: [{ props: 'never' }],
errors: [{ messageId: 'unnecessaryCurly' }],
},
{
code: '<App>{`foo`}</App>',
output: '<App>foo</App>',
options: [{ children: 'never' }],
errors: [{ messageId: 'unnecessaryCurly' }],
},
{
code: '<>{`foo`}</>',
output: '<>foo</>',
features: ['fragment'],
options: [{ children: 'never' }],
errors: [{ messageId: 'unnecessaryCurly' }],
},
{
code: `<MyComponent>{'foo'}</MyComponent>`,
output: '<MyComponent>foo</MyComponent>',
errors: [{ messageId: 'unnecessaryCurly' }],
},
{
code: `<MyComponent prop={'bar'}>foo</MyComponent>`,
output: `<MyComponent prop="bar">foo</MyComponent>`,
errors: [{ messageId: 'unnecessaryCurly' }],
},
{
code: `<MyComponent>{'foo'}</MyComponent>`,
output: '<MyComponent>foo</MyComponent>',
options: [{ children: 'never' }],
errors: [{ messageId: 'unnecessaryCurly' }],
},
{
code: `<MyComponent prop={'bar'}>foo</MyComponent>`,
output: '<MyComponent prop="bar">foo</MyComponent>',
options: [{ props: 'never' }],
errors: [{ messageId: 'unnecessaryCurly' }],
},
{
code: `
<MyComponent>
{'%'}
</MyComponent>
`,
output: `
<MyComponent>
%
</MyComponent>
`,
options: [{ children: 'never' }],
errors: [{ messageId: 'unnecessaryCurly' }],
},
{
code: `
<MyComponent>
{'foo'}
<div>
{'bar'}
</div>
{'baz'}
</MyComponent>
`,
output: `
<MyComponent>
foo
<div>
bar
</div>
baz
</MyComponent>
`,
options: [{ children: 'never' }],
errors: [
{ messageId: 'unnecessaryCurly' },
{ messageId: 'unnecessaryCurly' },
{ messageId: 'unnecessaryCurly' },
],
},
{
code: `
<MyComponent>
{'foo'}
<div>
{'bar'}
</div>
{'baz'}
{'some-complicated-exp'}
</MyComponent>
`,
output: `
<MyComponent>
foo
<div>
bar
</div>
{'baz'}
{'some-complicated-exp'}
</MyComponent>
`,
features: ['no-default', 'no-ts-new', 'no-babel-new'], // TODO: FIXME: remove no-default and no-ts-new and fix
options: [{ children: 'never' }],
errors: [
{ messageId: 'unnecessaryCurly', line: 3 },
{ messageId: 'unnecessaryCurly', line: 5 },
],
},
semver.satisfies(eslintPkg.version, '^7.5.0')
? {
// require('@babel/eslint-parser/package.json').peerDependencies.eslint
// TODO: figure out how to make all other parsers work this well
code: `
<MyComponent>
{'foo'}
<div>
{'bar'}
</div>
{'baz'}
{'some-complicated-exp'}
</MyComponent>
`,
output: `
<MyComponent>
foo
<div>
bar
</div>
baz
some-complicated-exp
</MyComponent>
`,
parser: parsers['@BABEL_ESLINT'],
parserOptions: parsers.babelParserOptions({}, new Set()),
options: [{ children: 'never' }],
errors: [
{ messageId: 'unnecessaryCurly', line: 3 },
{ messageId: 'unnecessaryCurly', line: 5 },
{ messageId: 'unnecessaryCurly', line: 7 },
{ messageId: 'unnecessaryCurly', line: 8 },
],
}
: [],
{
code: `<MyComponent prop='bar'>foo</MyComponent>`,
output: '<MyComponent prop={"bar"}>foo</MyComponent>',
options: [{ props: 'always' }],
errors: [{ messageId: 'missingCurly' }],
},
{
code: `<MyComponent prop="foo 'bar'">foo</MyComponent>`,
output: `<MyComponent prop={"foo 'bar'"}>foo</MyComponent>`,
options: [{ props: 'always' }],
errors: [{ messageId: 'missingCurly' }],
},
{
code: `<MyComponent prop='foo "bar"'>foo</MyComponent>`,
output: `<MyComponent prop={"foo \\"bar\\""}>foo</MyComponent>`,
options: [{ props: 'always' }],
errors: [{ messageId: 'missingCurly' }],
},
{
code: `<MyComponent prop="foo 'bar'">foo</MyComponent>`,
output: `<MyComponent prop={"foo 'bar'"}>foo</MyComponent>`,
options: [{ props: 'always' }],
errors: [{ messageId: 'missingCurly' }],
},
{
code: '<MyComponent>foo bar </MyComponent>',
output: `<MyComponent>{"foo bar "}</MyComponent>`,
options: [{ children: 'always' }],
errors: [{ messageId: 'missingCurly' }],
},
{
code: `<MyComponent prop="foo 'bar' \\n ">foo</MyComponent>`,
output: `<MyComponent prop={"foo 'bar' \\\\n "}>foo</MyComponent>`,
options: [{ props: 'always' }],
errors: [{ messageId: 'missingCurly' }],
},
{
code: '<MyComponent>foo bar \\r </MyComponent>',
output: '<MyComponent>{"foo bar \\\\r "}</MyComponent>',
options: [{ children: 'always' }],
errors: [{ messageId: 'missingCurly' }],
},
{
code: `<MyComponent>foo bar 'foo'</MyComponent>`,
output: `<MyComponent>{"foo bar 'foo'"}</MyComponent>`,
options: [{ children: 'always' }],
errors: [{ messageId: 'missingCurly' }],
},
{
code: '<MyComponent>foo bar "foo"</MyComponent>',
output: '<MyComponent>{"foo bar \\"foo\\""}</MyComponent>',
options: [{ children: 'always' }],
errors: [{ messageId: 'missingCurly' }],
},
{
code: '<MyComponent>foo bar <App/></MyComponent>',
output: '<MyComponent>{"foo bar "}<App/></MyComponent>',
options: [{ children: 'always' }],
errors: [{ messageId: 'missingCurly' }],
},
{
code: '<MyComponent>foo \\n bar</MyComponent>',
output: '<MyComponent>{"foo \\\\n bar"}</MyComponent>',
options: [{ children: 'always' }],
errors: [{ messageId: 'missingCurly' }],
},
{
code: '<MyComponent>foo \\u1234 bar</MyComponent>',
output: '<MyComponent>{"foo \\\\u1234 bar"}</MyComponent>',
options: [{ children: 'always' }],
errors: [{ messageId: 'missingCurly' }],
},
{
code: `<MyComponent prop='foo \\u1234 bar' />`,
output: '<MyComponent prop={"foo \\\\u1234 bar"} />',
options: [{ props: 'always' }],
errors: [{ messageId: 'missingCurly' }],
},
{
code: `<MyComponent prop={'bar'}>{'foo'}</MyComponent>`,
output: '<MyComponent prop="bar">foo</MyComponent>',
options: ['never'],
errors: [
{ messageId: 'unnecessaryCurly' },
{ messageId: 'unnecessaryCurly' },
],
},
{
code: `<MyComponent prop='bar'>foo</MyComponent>`,
output: '<MyComponent prop={"bar"}>{"foo"}</MyComponent>',
options: ['always'],
errors: [{ messageId: 'missingCurly' }, { messageId: 'missingCurly' }],
},
{
code: `<App prop={'foo'} attr={" foo "} />`,
output: '<App prop="foo" attr=" foo " />',
errors: [
{ messageId: 'unnecessaryCurly' },
{ messageId: 'unnecessaryCurly' },
],
options: [{ props: 'never' }],
},
{
code: `<App prop='foo' attr="bar" />`,
output: '<App prop={"foo"} attr={"bar"} />',
errors: [{ messageId: 'missingCurly' }, { messageId: 'missingCurly' }],
options: [{ props: 'always' }],
},
{
code: `<App prop='foo' attr={"bar"} />`,
output: `<App prop={"foo"} attr={"bar"} />`,
errors: [{ messageId: 'missingCurly' }],
options: [{ props: 'always' }],
},
{
code: `<App prop={'foo'} attr='bar' />`,
output: `<App prop={'foo'} attr={"bar"} />`,
errors: [{ messageId: 'missingCurly' }],
options: [{ props: 'always' }],
},
{
code: `<App prop='foo · bar' />`,
errors: [{ messageId: 'missingCurly' }],
options: [{ props: 'always' }],
output: `<App prop={"foo · bar"} />`,
},
{
code: '<App>foo · bar</App>',
errors: [{ messageId: 'missingCurly' }],
options: [{ children: 'always' }],
output: '<App>{"foo · bar"}</App>',
},
{
code: `<App>{'foo "bar"'}</App>`,
output: `<App>foo "bar"</App>`,
errors: [{ messageId: 'unnecessaryCurly' }],
options: [{ children: 'never' }],
},
{
code: `<App>{"foo 'bar'"}</App>`,
output: `<App>foo 'bar'</App>`,
errors: [{ messageId: 'unnecessaryCurly' }],
options: [{ children: 'never' }],
},
{
code: `
<App prop="${' '}
a${' '}
b c
d
">
a
b c${' '}
d${' '}
</App>
`,
errors: [{ messageId: 'missingCurly' }, { messageId: 'missingCurly' }],
options: ['always'],
output: `
<App prop="${' '}
a${' '}
b c
d
">
{"a"}
{"b c "}
{"d "}
</App>
`,
},
{
code: `
<App prop='${' '}
a${' '}
b c
d
'>
a
b c${' '}
d${' '}
</App>
`,
errors: [{ messageId: 'missingCurly' }, { messageId: 'missingCurly' }],
options: ['always'],
output: `
<App prop='${' '}
a${' '}
b c
d
'>
{"a"}
{"b c "}
{"d "}
</App>
`,
},
{
code: `
<App>
foo bar
<div>foo bar foo</div>
<span>
foo bar <i>foo bar</i>
<strong>
foo bar
</strong>
</span>
</App>
`,
output: `
<App>
{"foo bar"}
<div>{"foo bar foo"}</div>
<span>
{"foo bar "}<i>{"foo bar"}</i>
<strong>
{"foo bar"}
</strong>
</span>
</App>
`,
errors: [
{ messageId: 'missingCurly' },
{ messageId: 'missingCurly' },
{ messageId: 'missingCurly' },
{ messageId: 'missingCurly' },
{ messageId: 'missingCurly' },
],
options: [{ children: 'always' }],
},
{
code: `
<App>
<Component>
<Component />
</App>
`,
output: `
<App>
<{"Component"}>
<Component />
</App>
`,
errors: [{ messageId: 'missingCurly' }],
options: [{ children: 'always' }],
},
{
code: `
<Box mb={'1rem'} />
`,
output: `
<Box mb="1rem" />
`,
errors: [{ messageId: 'unnecessaryCurly' }],
options: [{ props: 'never' }],
},
{
code: `
<Box mb={'1rem {}'} />
`,
output: `
<Box mb="1rem {}" />
`,
errors: [{ messageId: 'unnecessaryCurly' }],
options: ['never'],
},
{
code: '<MyComponent prop={"{ style: true }"}>bar</MyComponent>',
output: '<MyComponent prop="{ style: true }">bar</MyComponent>',
errors: [{ messageId: 'unnecessaryCurly' }],
options: ['never'],
},
{
code: '<MyComponent prop={"< style: true >"}>foo</MyComponent>',
output: '<MyComponent prop="< style: true >">foo</MyComponent>',
errors: [{ messageId: 'unnecessaryCurly' }],
options: ['never'],
},
{
code: `<App horror=<div /> />`,
output: `<App horror={<div />} />`,
errors: [{ messageId: 'missingCurly' }],
options: [
{ props: 'always', children: 'always', propElementValues: 'always' },
],
features: ['no-ts'],
},
{
code: `<App horror={<div />} />`,
output: `<App horror=<div /> />`,
errors: [{ messageId: 'unnecessaryCurly' }],
options: [
{ props: 'never', children: 'never', propElementValues: 'never' },
],
features: ['no-ts'],
}
)
),
});