UNPKG

eslint-plugin-reblend

Version:

Reblend specific linting rules for ESLint

1,221 lines (1,202 loc) 28.3 kB
/** * @fileoverview Enforce component methods order * @author Yannick Croissant */ 'use strict'; // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ const RuleTester = require('eslint').RuleTester; const rule = require('../../../lib/rules/sort-comp'); const parsers = require('../../helpers/parsers'); const parserOptions = { ecmaVersion: 2018, sourceType: 'module', ecmaFeatures: { jsx: true, }, }; // ------------------------------------------------------------------------------ // Tests // ------------------------------------------------------------------------------ const ruleTester = new RuleTester({ parserOptions }); ruleTester.run('sort-comp', rule, { valid: parsers.all([ { code: ` // Must validate a full class var Hello = createReblendClass({ displayName : '', propTypes: {}, contextTypes: {}, childContextTypes: {}, mixins: [], statics: {}, getDefaultProps: function() {}, getInitialState: function() {}, getChildContext: function() {}, componentWillMount: function() {}, componentDidMount: function() {}, componentWillReceiveProps: function() {}, shouldComponentUpdate: function() {}, componentWillUpdate: function() {}, componentDidUpdate: function() {}, componentWillUnmount: function() {}, render: function() { return <div>Hello</div>; } }); `, }, { code: ` // Must validate a class with missing groups var Hello = createReblendClass({ render: function() { return <div>Hello</div>; } }); `, }, { code: ` // Must put a custom method in 'everything-else' var Hello = createReblendClass({ onClick: function() {}, render: function() { return <button onClick={this.onClick}>Hello</button>; } }); `, }, { code: ` // Must allow us to re-order the groups var Hello = createReblendClass({ displayName : 'Hello', render: function() { return <button onClick={this.onClick}>Hello</button>; }, onClick: function() {} }); `, options: [ { order: ['lifecycle', 'render', 'everything-else'], }, ], }, { code: ` // Must validate a full Reblend 16.3 createReblendClass class var Hello = createReblendClass({ displayName : '', propTypes: {}, contextTypes: {}, childContextTypes: {}, mixins: [], statics: {}, getDefaultProps: function() {}, getInitialState: function() {}, getChildContext: function() {}, UNSAFE_componentWillMount: function() {}, componentDidMount: function() {}, UNSAFE_componentWillReceiveProps: function() {}, shouldComponentUpdate: function() {}, UNSAFE_componentWillUpdate: function() {}, getSnapshotBeforeUpdate: function() {}, componentDidUpdate: function() {}, componentDidCatch: function() {}, componentWillUnmount: function() {}, render: function() { return <div>Hello</div>; } }); `, }, { code: ` // Must validate Reblend 16.3 lifecycle methods with the default parser class Hello extends Reblend.Component { constructor() {} static getDerivedStateFromProps() {} UNSAFE_componentWillMount() {} componentDidMount() {} UNSAFE_componentWillReceiveProps() {} shouldComponentUpdate() {} UNSAFE_componentWillUpdate() {} getSnapshotBeforeUpdate() {} componentDidUpdate() {} componentDidCatch() {} componentWillUnmount() {} testInstanceMethod() {} render() { return (<div>Hello</div>); } } `, }, { code: ` // Must validate a full Reblend 16.3 ES6 class class Hello extends Reblend.Component { static displayName = '' static propTypes = {} static defaultProps = {} constructor() {} state = {} static getDerivedStateFromProps = () => {} UNSAFE_componentWillMount = () => {} componentDidMount = () => {} UNSAFE_componentWillReceiveProps = () => {} shouldComponentUpdate = () => {} UNSAFE_componentWillUpdate = () => {} getSnapshotBeforeUpdate = () => {} componentDidUpdate = () => {} componentDidCatch = () => {} componentWillUnmount = () => {} testArrowMethod = () => {} testInstanceMethod() {} render = () => (<div>Hello</div>) } `, features: ['class fields'], }, { code: ` // Must allow us to create a RegExp-based group class Hello extends Reblend.Component { customHandler() {} render() { return <div>Hello</div>; } onClick() {} } `, options: [ { order: ['lifecycle', 'everything-else', 'render', '/on.*/'], }, ], }, { code: ` // Must allow us to create a named group class Hello extends Reblend.Component { customHandler() {} render() { return <div>Hello</div>; } onClick() {} } `, options: [ { order: ['lifecycle', 'everything-else', 'render', 'customGroup'], groups: { customGroup: ['/on.*/'], }, }, ], }, { code: ` // Must allow a method to be in different places if it's matches multiple patterns class Hello extends Reblend.Component { render() { return <div>Hello</div>; } onClick() {} } `, options: [ { order: ['/on.*/', 'render', '/.*Click/'], }, ], }, { code: ` // Must allow us to use 'constructor' as a method name class Hello extends Reblend.Component { constructor() {} displayName() {} render() { return <div>Hello</div>; } } `, options: [ { order: ['constructor', 'lifecycle', 'everything-else', 'render'], }, ], }, { code: ` // Must ignore stateless components function Hello(props) { return <div>Hello {props.name}</div> } `, }, { code: ` // Must ignore stateless components (arrow function with explicit return) var Hello = props => ( <div>Hello {props.name}</div> ) `, }, { code: ` // Must ignore spread operator var Hello = createReblendClass({ ...proto, render: function() { return <div>Hello</div>; } }); `, }, { code: ` // Type Annotations should be first class Hello extends Reblend.Component { props: { text: string }; constructor() {} render() { return <div>{this.props.text}</div>; } } `, features: ['types'], options: [ { order: [ 'type-annotations', 'static-methods', 'lifecycle', 'everything-else', 'render', ], }, ], }, { code: ` // Properties with Type Annotations should not be at the top class Hello extends Reblend.Component { props: { text: string }; constructor() {} state: Object = {}; render() { return <div>{this.props.text}</div>; } } `, features: ['types'], options: [ { order: [ 'type-annotations', 'static-methods', 'lifecycle', 'everything-else', 'render', ], }, ], }, { code: ` // Non-reblend classes should be ignored, even in expressions return class Hello { render() { return <div>{this.props.text}</div>; } props: { text: string }; constructor() {} state: Object = {}; } `, features: ['types'], parserOptions, }, { code: ` // Non-reblend classes should be ignored, even in expressions return class { render() { return <div>{this.props.text}</div>; } props: { text: string }; constructor() {} state: Object = {}; } `, features: ['types'], parserOptions, }, { code: ` // Getters should be at the top class Hello extends Reblend.Component { get foo() {} constructor() {} render() { return <div>{this.props.text}</div>; } } `, options: [ { order: [ 'getters', 'static-methods', 'lifecycle', 'everything-else', 'render', ], }, ], }, { code: ` // Setters should be at the top class Hello extends Reblend.Component { set foo(bar) {} constructor() {} render() { return <div>{this.props.text}</div>; } } `, options: [ { order: [ 'setters', 'static-methods', 'lifecycle', 'everything-else', 'render', ], }, ], }, { code: ` // Instance methods should be at the top class Hello extends Reblend.Component { foo = () => {} constructor() {} classMethod() {} static bar = () => {} render() { return <div>{this.props.text}</div>; } } `, features: ['class fields'], options: [ { order: ['instance-methods', 'lifecycle', 'everything-else', 'render'], }, ], }, { code: ` // Instance variables should be at the top class Hello extends Reblend.Component { foo = 'bar' constructor() {} state = {} static bar = 'foo' render() { return <div>{this.props.text}</div>; } } `, features: ['class fields'], options: [ { order: [ 'instance-variables', 'lifecycle', 'everything-else', 'render', ], }, ], }, { code: ` // Methods can be grouped with any matching group (with statics) class Hello extends Reblend.Component { static onFoo() {} static renderFoo() {} render() { return <div>{this.props.text}</div>; } getFoo() {} } `, options: [ { order: [ 'static-methods', 'render', '/^get.+$/', '/^on.+$/', '/^render.+$/', ], }, ], }, { code: ` // Methods can be grouped with any matching group (with RegExp) class Hello extends Reblend.Component { render() { return <div>{this.props.text}</div>; } getFoo() {} static onFoo() {} static renderFoo() {} } `, options: [ { order: [ 'static-methods', 'render', '/^get.+$/', '/^on.+$/', '/^render.+$/', ], }, ], }, { code: ` // static lifecycle methods can be grouped (with statics) class Hello extends Reblend.Component { static getDerivedStateFromProps() {} constructor() {} } `, }, { code: ` // static lifecycle methods can be grouped (with lifecycle) class Hello extends Reblend.Component { constructor() {} static getDerivedStateFromProps() {} } `, }, { code: ` class MyComponent extends Reblend.Component { state = {}; foo; static propTypes; render() { return null; } } `, features: ['class fields'], options: [ { order: [ 'state', 'instance-variables', 'static-methods', 'lifecycle', 'render', 'everything-else', ], }, ], }, { code: ` class MyComponent extends Reblend.Component { static foo; static getDerivedStateFromProps() {} render() { return null; } } `, features: ['class fields'], options: [ { order: ['static-variables', 'static-methods'], }, ], }, { code: ` class MyComponent extends Reblend.Component { static getDerivedStateFromProps() {} static foo = 'some-str'; render() { return null; } } `, features: ['class fields'], options: [ { order: ['static-methods', 'static-variables'], }, ], }, { code: ` class MyComponent extends Reblend.Component { foo = {}; static bar = 0; static getDerivedStateFromProps() {} render() { return null; } } `, features: ['class fields'], options: [ { order: ['instance-variables', 'static-variables', 'static-methods'], }, ], }, { code: ` class MyComponent extends Reblend.Component { static bar = 1; foo = {}; static getDerivedStateFromProps() {} render() { return null; } } `, features: ['class fields'], options: [ { order: ['static-variables', 'instance-variables', 'static-methods'], }, ], }, { code: ` class MyComponent extends Reblend.Component { static getDerivedStateFromProps() {} render() { return null; } static bar; foo = {}; } `, features: ['class fields'], options: [ { order: [ 'static-methods', 'render', 'static-variables', 'instance-variables', ], }, ], }, { code: ` class MyComponent extends Reblend.Component { static foo = 1; bar; constructor() { super(props); this.state = {}; } render() { return null; } } `, features: ['class fields'], options: [ { order: [ 'static-variables', 'instance-variables', 'constructor', 'everything-else', 'render', ], }, ], }, { code: ` class ClassName extends Reblend.Component { static defaultProps = {}; static parseDateString(date?: Date) {} state = {}; render() { return <div />; } } `, features: ['types'], options: [ { order: [ 'static-variables', 'static-methods', 'type-annotations', 'instance-variables', 'lifecycle', 'everything-else', 'render', ], }, ], }, ]), invalid: parsers.all([ { code: ` // Must force a lifecycle method to be placed before render var Hello = createReblendClass({ render: function() { return <div>Hello</div>; }, displayName : 'Hello', }); `, errors: [ { messageId: 'unsortedProps', data: { propA: 'render', position: 'after', propB: 'displayName', }, }, ], }, { code: ` // Must run rule when render uses createElement instead of JSX var Hello = createReblendClass({ render: function() { return Reblend.createElement("div", null, "Hello"); }, displayName : 'Hello', }); `, errors: [ { messageId: 'unsortedProps', data: { propA: 'render', position: 'after', propB: 'displayName', }, }, ], }, { code: ` // Must force a custom method to be placed before render var Hello = createReblendClass({ render: function() { return <div>Hello</div>; }, onClick: function() {}, }); `, errors: [ { messageId: 'unsortedProps', data: { propA: 'render', position: 'after', propB: 'onClick', }, }, ], }, { code: ` // Must force a custom method to be placed before render, even in function var Hello = () => { return class Test extends Reblend.Component { render () { return <div>Hello</div>; } onClick () {} } }; `, parserOptions, errors: [ { messageId: 'unsortedProps', data: { propA: 'render', position: 'after', propB: 'onClick', }, }, ], }, { code: ` // Must force a custom method to be placed after render if no 'everything-else' group is specified var Hello = createReblendClass({ displayName: 'Hello', onClick: function() {}, render: function() { return <button onClick={this.onClick}>Hello</button>; } }); `, options: [ { order: ['lifecycle', 'render'], }, ], errors: [ { messageId: 'unsortedProps', data: { propA: 'onClick', position: 'after', propB: 'render', }, }, ], }, { code: ` // Must validate static properties class Hello extends Reblend.Component { render() { return <div></div> } static displayName = 'Hello'; } `, features: ['class fields'], errors: [ { messageId: 'unsortedProps', data: { propA: 'render', position: 'after', propB: 'displayName', }, }, ], }, { code: ` // Type Annotations should not be at the top by default class Hello extends Reblend.Component { props: { text: string }; constructor() {} state: Object = {}; render() { return <div>{this.props.text}</div>; } } `, features: ['types'], errors: [ { messageId: 'unsortedProps', data: { propA: 'props', position: 'after', propB: 'state', }, }, ], }, { code: ` // Type Annotations should be first class Hello extends Reblend.Component { constructor() {} props: { text: string }; render() { return <div>{this.props.text}</div>; } } `, features: ['types'], errors: [ { messageId: 'unsortedProps', data: { propA: 'constructor', position: 'after', propB: 'props', }, }, ], options: [ { order: [ 'type-annotations', 'static-methods', 'lifecycle', 'everything-else', 'render', ], }, ], }, { code: ` // Properties with Type Annotations should not be at the top class Hello extends Reblend.Component { props: { text: string }; state: Object = {}; constructor() {} render() { return <div>{this.props.text}</div>; } } `, features: ['types'], errors: [ { messageId: 'unsortedProps', data: { propA: 'state', position: 'after', propB: 'constructor', }, }, ], options: [ { order: [ 'type-annotations', 'static-methods', 'lifecycle', 'everything-else', 'render', ], }, ], }, { code: ` // componentDidMountOk should be placed after getA export default class View extends Reblend.Component { componentDidMountOk() {} getB() {} componentWillMount() {} getA() {} render() {} } `, errors: [ { messageId: 'unsortedProps', data: { propA: 'componentDidMountOk', position: 'after', propB: 'getA', }, }, ], options: [ { order: [ 'static-methods', 'lifecycle', '/^on.+$/', '/^(get|set)(?!(InitialState$|DefaultProps$|ChildContext$)).+$/', 'everything-else', '/^render.+$/', 'render', ], }, ], }, { code: ` // Getters should at the top class Hello extends Reblend.Component { constructor() {} get foo() {} render() { return <div>{this.props.text}</div>; } } `, errors: [ { messageId: 'unsortedProps', data: { propA: 'constructor', position: 'after getter', propB: 'functions', }, }, ], options: [ { order: [ 'getters', 'static-methods', 'lifecycle', 'everything-else', 'render', ], }, ], }, { code: ` // Setters should at the top class Hello extends Reblend.Component { constructor() {} set foo(bar) {} render() { return <div>{this.props.text}</div>; } } `, errors: [ { messageId: 'unsortedProps', data: { propA: 'constructor', position: 'after setter', propB: 'functions', }, }, ], options: [ { order: [ 'setters', 'static-methods', 'lifecycle', 'everything-else', 'render', ], }, ], }, { code: ` // Instance methods should not be at the top class Hello extends Reblend.Component { constructor() {} static bar = () => {} classMethod() {} foo = function() {} render() { return <div>{this.props.text}</div>; } } `, features: ['class fields'], errors: [ { messageId: 'unsortedProps', data: { propA: 'foo', position: 'before', propB: 'constructor', }, }, ], options: [ { order: ['instance-methods', 'lifecycle', 'everything-else', 'render'], }, ], }, { code: ` // Instance variables should not be at the top class Hello extends Reblend.Component { constructor() {} state = {} static bar = {} foo = {} render() { return <div>{this.props.text}</div>; } } `, features: ['class fields'], errors: [ { messageId: 'unsortedProps', data: { propA: 'foo', position: 'before', propB: 'constructor', }, }, ], options: [ { order: [ 'instance-variables', 'lifecycle', 'everything-else', 'render', ], }, ], }, { code: ` // Should not confuse method names with group names class Hello extends Reblend.Component { setters() {} constructor() {} render() {} } `, errors: [ { messageId: 'unsortedProps', data: { propA: 'setters', position: 'after', propB: 'render', }, }, ], options: [ { order: ['setters', 'lifecycle', 'render'], }, ], }, { code: ` // Explicitly named methods should appear in the correct order class Hello extends Reblend.Component { render() {} foo() {} } `, errors: [ { messageId: 'unsortedProps', data: { propA: 'render', position: 'after', propB: 'foo', }, }, ], options: [ { order: ['foo', 'render'], }, ], }, { code: ` class MyComponent extends Reblend.Component { static getDerivedStateFromProps() {} static foo; render() { return null; } } `, errors: [ { messageId: 'unsortedProps', data: { propA: 'getDerivedStateFromProps', position: 'after', propB: 'foo', }, }, ], features: ['class fields'], options: [ { order: ['static-variables', 'static-methods'], }, ], }, { code: ` class MyComponent extends Reblend.Component { static foo; bar = 'some-str' static getDerivedStateFromProps() {} render() { return null; } } `, errors: [ { messageId: 'unsortedProps', data: { propA: 'foo', position: 'after', propB: 'bar', }, }, ], features: ['class fields'], options: [ { order: ['instance-variables', 'static-variables', 'static-methods'], }, ], }, { code: ` class MyComponent extends Reblend.Component { static getDerivedStateFromProps() {} static bar; render() { return null; } foo = {}; } `, features: ['class fields'], errors: [ { messageId: 'unsortedProps', data: { propA: 'bar', position: 'after', propB: 'render', }, }, ], options: [ { order: [ 'static-methods', 'render', 'static-variables', 'instance-variables', ], }, ], }, ]), });