diff --git a/rules/sort-classes.ts b/rules/sort-classes.ts index a81bd716a..c33ba2eb1 100644 --- a/rules/sort-classes.ts +++ b/rules/sort-classes.ts @@ -1,3 +1,4 @@ +import type { TSESTree } from '@typescript-eslint/types' import type { TSESLint } from '@typescript-eslint/utils' import type { SortingNode } from '../typings' @@ -186,6 +187,55 @@ export default createEslintRule({ let sourceCode = getSourceCode(context) + let extractDependencies = ( + expression: TSESTree.Expression, + ): string[] => { + let dependencies: string[] = [] + + let checkNode = (nodeValue: TSESTree.Node) => { + if ( + nodeValue.type === 'MemberExpression' && + nodeValue.object.type === 'ThisExpression' && + nodeValue.property.type === 'Identifier' + ) { + dependencies.push(nodeValue.property.name) + } + + if ('body' in nodeValue && nodeValue.body) { + traverseNode(nodeValue.body) + } + + if ('left' in nodeValue) { + traverseNode(nodeValue.left) + } + + if ('right' in nodeValue) { + traverseNode(nodeValue.right) + } + + if ('elements' in nodeValue) { + nodeValue.elements + .filter(currentNode => currentNode !== null) + .forEach(traverseNode) + } + + if ('arguments' in nodeValue) { + nodeValue.arguments.forEach(traverseNode) + } + } + + let traverseNode = (nodeValue: TSESTree.Node[] | TSESTree.Node) => { + if (Array.isArray(nodeValue)) { + nodeValue.forEach(traverseNode) + } else { + checkNode(nodeValue) + } + } + + traverseNode(expression) + return dependencies + } + let formattedNodes: SortingNode[][] = node.body.reduce( (accumulator: SortingNode[][], member) => { let comment = getCommentBefore(member, sourceCode) @@ -199,6 +249,7 @@ export default createEslintRule({ } let name: string + let dependencies: string[] = [] let { getGroup, defineGroup, setCustomGroups } = useGroups( options.groups, ) @@ -299,9 +350,14 @@ export default createEslintRule({ override: true, }) + if (member.type === 'PropertyDefinition' && member.value) { + dependencies = extractDependencies(member.value) + } + let value = { size: rangeToDiff(member.range), group: getGroup(), + dependencies, node: member, name, } diff --git a/test/sort-classes.test.ts b/test/sort-classes.test.ts index 1dd2b3e4f..96754b025 100644 --- a/test/sort-classes.test.ts +++ b/test/sort-classes.test.ts @@ -741,7 +741,7 @@ describe(ruleName, () => { valid: [ { code: dedent` - export class Example { + class Class { // Region: Table protected onChangeColumns() {} @@ -773,7 +773,7 @@ describe(ruleName, () => { invalid: [ { code: dedent` - export class Example { + class Class { // Region: Table protected onChangeColumns() {} @@ -795,7 +795,7 @@ describe(ruleName, () => { } `, output: dedent` - export class Example { + class Class { // Region: Table protected onChangeColumns() {} @@ -848,428 +848,208 @@ describe(ruleName, () => { }, ], }) - }) - - describe(`${ruleName}: sorting by natural order`, () => { - let type = 'natural-order' - - let options = { - ignoreCase: true, - type: 'natural', - order: 'asc', - } as const - - ruleTester.run(`${ruleName}(${type}): sorts class members`, rule, { - valid: [ - { - code: dedent` - class Class { - static a = 'a' - - private b = 'b' - - c = 'c' - - d = 'd' - - constructor() {} - - static e() {} - - private f() {} - - g() {} - - h() {} - } - `, - options: [ - { - ...options, - groups: [ - 'static-property', - 'private-property', - 'property', - 'constructor', - 'static-method', - 'private-method', - 'method', - 'unknown', - ], - }, - ], - }, - ], - invalid: [ - { - code: dedent` - class Class { - static a = 'a' - - private b = 'b' - - d = 'd' - - c = 'c' - - static e() {} - - constructor() {} - - private f() {} - - g() {} - - h() {} - } - `, - output: dedent` - class Class { - static a = 'a' - - private b = 'b' - - c = 'c' - - d = 'd' - - constructor() {} - - static e() {} - - private f() {} - - g() {} - - h() {} - } - `, - options: [ - { - ...options, - groups: [ - 'static-property', - 'private-property', - 'property', - 'constructor', - 'static-method', - 'private-method', - 'method', - 'unknown', - ], - }, - ], - errors: [ - { - messageId: 'unexpectedClassesOrder', - data: { - left: 'd', - right: 'c', - }, - }, - { - messageId: 'unexpectedClassesOrder', - data: { - left: 'e', - right: 'constructor', - }, - }, - ], - }, - ], - }) ruleTester.run( - `${ruleName}(${type}): sorts class and group members`, + `${ruleName}(${type}): does not sort properties if the right value depends on the left value`, rule, { valid: [ { code: dedent` class Class { - static a - - static b = 'b' + b = 'b' - [key in O] + aaa = [this.b] + } + `, + options: [options], + }, + { + code: dedent` + class Class { + b = 'b' - static { - this.a = 'd' + getAaa() { + return this.b; } } `, - options: [ - { - ...options, - groups: [ - ['static-property', 'private-property', 'property'], - 'constructor', - ['static-method', 'private-method', 'method'], - 'unknown', - ], - }, - ], + options: [options], }, - ], - invalid: [ { code: dedent` class Class { - [key in O] + static c = 'c' - static b = 'b' - - static a + b = Example.c + } + `, + options: [options], + }, + { + code: dedent` + class Class { + #b = 'b' - static { - this.a = 'd' + getAaa() { + return this.#b; } } `, - output: dedent` + options: [options], + }, + { + code: dedent` class Class { - static a - static b = 'b' - [key in O] - - static { - this.a = 'd' + static getAaa() { + return this.b; } } `, - options: [ - { - ...options, - groups: [ - ['static-property', 'private-property', 'property'], - 'constructor', - ['static-method', 'private-method', 'method'], - 'unknown', - ], - }, - ], + options: [options], + }, + ], + invalid: [ + { + code: dedent` + class Class { + aaa = [this.b] + + b = 'b' + } + `, + output: dedent` + class Class { + b = 'b' + + aaa = [this.b] + } + `, + options: [options], errors: [ { messageId: 'unexpectedClassesOrder', data: { - left: 'key in O', + left: 'aaa', right: 'b', }, }, - { - messageId: 'unexpectedClassesOrder', - data: { - left: 'b', - right: 'a', - }, - }, ], }, - ], - }, - ) - - ruleTester.run( - `${ruleName}(${type}): sorts class with ts index signatures`, - rule, - { - valid: [ { code: dedent` class Class { - static a = 'a'; + getAaa() { + return this.b; + } - [k: string]: any; + b = 'b' + } + `, + output: dedent` + class Class { + b = 'b' - [k: string]; + getAaa() { + return this.b; + } } `, - options: [ + options: [options], + errors: [ { - ...options, - groups: [ - ['static-property', 'private-property', 'property'], - 'constructor', - ], + messageId: 'unexpectedClassesOrder', + data: { + left: 'getAaa', + right: 'b', + }, }, ], }, - ], - invalid: [ { code: dedent` class Class { - [k: string]: any; - - [k: string]; + b = Example.c - static a = 'a'; + static c = 'c' } `, output: dedent` class Class { - static a = 'a'; - - [k: string]: any; + static c = 'c' - [k: string]; + b = Example.c } `, - options: [ - { - ...options, - groups: [ - ['static-property', 'private-property', 'property'], - 'constructor', - ], - }, - ], - errors: [ + options: [options], + errors: [ { messageId: 'unexpectedClassesOrder', data: { - left: '[k: string];', - right: 'a', + left: 'b', + right: 'c', }, }, ], }, - ], - }, - ) - - ruleTester.run( - `${ruleName}(${type}): sorts class with ts index signatures`, - rule, - { - valid: [ { code: dedent` - class Decorations { - setBackground(color: number, hexFlag: boolean): this - setBackground(color: Color | string | CSSColor): this - setBackground(r: number, g: number, b: number, a?: number): this - setBackground(color: ColorArgument, arg1?: boolean | number, arg2?: number, arg3?: number): this { - /* ... */ + class Class { + getAaa() { + return this.#b; } + + #b = 'b' } `, - options: [ + output: dedent` + class Class { + #b = 'b' + + getAaa() { + return this.#b; + } + } + `, + options: [options], + errors: [ { - ...options, - groups: [ - ['static-property', 'private-property', 'property'], - 'constructor', - ], + messageId: 'unexpectedClassesOrder', + data: { + left: 'getAaa', + right: '#b', + }, }, ], }, - ], - invalid: [], - }, - ) - - ruleTester.run( - `${ruleName}(${type}): sorts private methods with hash`, - rule, - { - valid: [], - invalid: [ { code: dedent` - class MyUnsortedClass { - someOtherProperty - - someProperty = 1 - - constructor() {} - - static #aPrivateStaticMethod () {} - - #somePrivateProperty - - #someOtherPrivateProperty = 2 - - static someStaticProperty = 3 - - static #someStaticPrivateProperty = 4 - - aInstanceMethod () {} - - static aStaticMethod () {} + class Class { + static getAaa() { + return this.b; + } - #aPrivateInstanceMethod () {} + static b = 'b' } `, output: dedent` - class MyUnsortedClass { - static someStaticProperty = 3 - - #someOtherPrivateProperty = 2 - - #somePrivateProperty - - static #someStaticPrivateProperty = 4 - - someOtherProperty - - someProperty = 1 - - constructor() {} - - aInstanceMethod () {} - - #aPrivateInstanceMethod () {} - - static #aPrivateStaticMethod () {} + class Class { + static b = 'b' - static aStaticMethod () {} + static getAaa() { + return this.b; + } } `, - options: [ - { - ...options, - groups: [ - 'static-property', - 'private-property', - 'property', - 'constructor', - 'method', - 'private-method', - 'static-method', - 'unknown', - ], - }, - ], + options: [options], errors: [ { messageId: 'unexpectedClassesOrder', data: { - left: '#aPrivateStaticMethod', - right: '#somePrivateProperty', - }, - }, - { - messageId: 'unexpectedClassesOrder', - data: { - left: '#somePrivateProperty', - right: '#someOtherPrivateProperty', - }, - }, - { - messageId: 'unexpectedClassesOrder', - data: { - left: '#someOtherPrivateProperty', - right: 'someStaticProperty', - }, - }, - { - messageId: 'unexpectedClassesOrder', - data: { - left: 'aStaticMethod', - right: '#aPrivateInstanceMethod', + left: 'getAaa', + right: 'b', }, }, ], @@ -1279,60 +1059,60 @@ describe(ruleName, () => { ) ruleTester.run( - `${ruleName}(${type}): allows split methods with getters and setters`, + `${ruleName}(${type}): works with left and right dependencies`, rule, { - valid: [], + valid: [ + { + code: dedent` + class Class { + left = 'left' + right = 'right' + + aaa = this.left + this.right + } + `, + options: [options], + }, + { + code: dedent` + class Class { + condition1 = true + condition2 = false + + result = this.condition1 && this.condition2 + } + `, + options: [options], + }, + ], invalid: [ { code: dedent` - class A { - x() {} - b() {} - get z() {} - get c() {} - set c() {} + class Class { + aaa = this.left + this.right + + left = 'left' + + right = 'right' } `, output: dedent` - class A { - b() {} - x() {} - get c() {} - set c() {} - get z() {} + class Class { + left = 'left' + + right = 'right' + + aaa = this.left + this.right } `, - options: [ - { - ...options, - groups: [ - 'index-signature', - 'static-property', - 'private-property', - 'property', - 'constructor', - 'static-method', - 'private-method', - 'method', - ['get-method', 'set-method'], - 'unknown', - ], - }, - ], + options: [options], errors: [ { messageId: 'unexpectedClassesOrder', data: { - left: 'x', - right: 'b', - }, - }, - { - messageId: 'unexpectedClassesOrder', - data: { - left: 'z', - right: 'c', + left: 'aaa', + right: 'left', }, }, ], @@ -1341,210 +1121,319 @@ describe(ruleName, () => { }, ) - ruleTester.run(`${ruleName}(${type}): sorts decorated properties`, rule, { - valid: [], - invalid: [ + ruleTester.run(`${ruleName}(${type}): works with body dependencies`, rule, { + valid: [ { code: dedent` - class User { - firstName: string + class Class { + a = 10 - id: number + method = function() { + const b = this.a + 20; + return b; + } + } + `, + options: [options], + }, + { + code: dedent` + class Class { + a = 10 - @Index({ name: 'born_index' }) - @Property() - born: string + method = () => { + const b = this.a + 20; + return b; + } + } + `, + options: [options], + }, + { + code: dedent` + class Class { + a = 10 + b = 20 + + method() { + { + const c = this.a + this.b; + console.log(c); + } + } + } + `, + options: [options], + }, + { + code: dedent` + class Class { + a = 10 + b = this.a + 20 - @Property() - @Unique() - email: string + method() { + return this.b; + } + } + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + class Class { + method = function() { + const b = this.a + 20; + return b; + } - lastName: string - }`, + a = 10 + } + `, output: dedent` - class User { - firstName: string - - id: number - - lastName: string - - @Index({ name: 'born_index' }) - @Property() - born: string + class Class { + a = 10 - @Property() - @Unique() - email: string - }`, - options: [ + method = function() { + const b = this.a + 20; + return b; + } + } + `, + options: [options], + errors: [ { - ...options, - groups: ['property', 'decorated-property', 'unknown'], + messageId: 'unexpectedClassesOrder', + data: { + left: 'method', + right: 'a', + }, }, ], + }, + { + code: dedent` + class Class { + method = () => { + const b = this.a + 20; + return b; + } + + a = 10 + } + `, + output: dedent` + class Class { + a = 10 + + method = () => { + const b = this.a + 20; + return b; + } + } + `, + options: [options], errors: [ { messageId: 'unexpectedClassesOrder', data: { - left: 'email', - right: 'lastName', + left: 'method', + right: 'a', }, }, ], }, { code: dedent` - class MyElement { - @property({ attribute: false }) - data = {} + class Class { + method() { + { + const c = this.a + this.b; + console.log(c); + } + } - @property() - greeting: string = 'Hello' + a = 10 - @state() - private _counter = 0 + b = 20 + } + `, + output: dedent` + class Class { + a = 10 - private _message = '' + b = 20 - private _prop = 0 + method() { + { + const c = this.a + this.b; + console.log(c); + } + } + } + `, + options: [options], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'method', + right: 'a', + }, + }, + ], + }, + { + code: dedent` + class Class { + method() { + return this.b; + } - constructor() {} + b = this.a + 20 - @property() - get message(): string { - return this._message + a = 10 } + `, + output: dedent` + class Class { + a = 10 - set message(message: string) { - this._message = message - } + b = this.a + 20 - @property() - set prop(val: number) { - this._prop = Math.floor(val) + method() { + return this.b; + } } + `, + options: [options], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'method', + right: 'b', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'b', + right: 'a', + }, + }, + ], + }, + ], + }) + }) - get prop() { - return this._prop - } + describe(`${ruleName}: sorting by natural order`, () => { + let type = 'natural-order' - render() {} - }`, - output: dedent` - class MyElement { - @property({ attribute: false }) - data = {} + let options = { + ignoreCase: true, + type: 'natural', + order: 'asc', + } as const - @property() - greeting: string = 'Hello' + ruleTester.run(`${ruleName}(${type}): sorts class members`, rule, { + valid: [ + { + code: dedent` + class Class { + static a = 'a' - @state() - private _counter = 0 + private b = 'b' - private _message = '' + c = 'c' - private _prop = 0 + d = 'd' constructor() {} - @property() - get message(): string { - return this._message - } - - @property() - set prop(val: number) { - this._prop = Math.floor(val) - } + static e() {} - set message(message: string) { - this._message = message - } + private f() {} - get prop() { - return this._prop - } + g() {} - render() {} - }`, + h() {} + } + `, options: [ { ...options, groups: [ - 'decorated-property', - 'property', - 'private-decorated-property', + 'static-property', 'private-property', + 'property', 'constructor', - ['decorated-get-method', 'decorated-set-method'], - ['get-method', 'set-method'], + 'static-method', + 'private-method', + 'method', 'unknown', ], }, ], - errors: [ - { - messageId: 'unexpectedClassesOrder', - data: { - left: 'message', - right: 'prop', - }, - }, - ], }, ], - }) - - ruleTester.run(`${ruleName}(${type}): sorts decorated accessors`, rule, { - valid: [], invalid: [ { code: dedent` - class Todo { - id = Math.random() + class Class { + static a = 'a' - constructor() {} + private b = 'b' - @action - toggle() {} + d = 'd' - @observable - accessor #active = false + c = 'c' - @observable - accessor finished = false + static e() {} - @observable - accessor title = '' - }`, + constructor() {} + + private f() {} + + g() {} + + h() {} + } + `, output: dedent` - class Todo { - @observable - accessor finished = false + class Class { + static a = 'a' - @observable - accessor title = '' + private b = 'b' - @observable - accessor #active = false + c = 'c' - id = Math.random() + d = 'd' constructor() {} - @action - toggle() {} - }`, + static e() {} + + private f() {} + + g() {} + + h() {} + } + `, options: [ { ...options, groups: [ - 'decorated-accessor-property', - 'private-decorated-accessor-property', + 'static-property', + 'private-property', 'property', 'constructor', - 'decorated-method', + 'static-method', + 'private-method', + 'method', 'unknown', ], }, @@ -1553,15 +1442,15 @@ describe(ruleName, () => { { messageId: 'unexpectedClassesOrder', data: { - left: 'toggle', - right: '#active', + left: 'd', + right: 'c', }, }, { messageId: 'unexpectedClassesOrder', data: { - left: '#active', - right: 'finished', + left: 'e', + right: 'constructor', }, }, ], @@ -1569,111 +1458,1180 @@ describe(ruleName, () => { ], }) - ruleTester.run(`${ruleName}(${type}): sorts decorated accessors`, rule, { - valid: [ - { - code: dedent` - export class Example { - // Region: Table - protected onChangeColumns() {} - - protected onPaginationChanged() {} - - protected onSortChanged() {} - + ruleTester.run( + `${ruleName}(${type}): sorts class and group members`, + rule, + { + valid: [ + { + code: dedent` + class Class { + static a + + static b = 'b' + + [key in O] + + static { + this.a = 'd' + } + } + `, + options: [ + { + ...options, + groups: [ + ['static-property', 'private-property', 'property'], + 'constructor', + ['static-method', 'private-method', 'method'], + 'unknown', + ], + }, + ], + }, + ], + invalid: [ + { + code: dedent` + class Class { + [key in O] + + static b = 'b' + + static a + + static { + this.a = 'd' + } + } + `, + output: dedent` + class Class { + static a + + static b = 'b' + + [key in O] + + static { + this.a = 'd' + } + } + `, + options: [ + { + ...options, + groups: [ + ['static-property', 'private-property', 'property'], + 'constructor', + ['static-method', 'private-method', 'method'], + 'unknown', + ], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'key in O', + right: 'b', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'b', + right: 'a', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): sorts class with ts index signatures`, + rule, + { + valid: [ + { + code: dedent` + class Class { + static a = 'a'; + + [k: string]: any; + + [k: string]; + } + `, + options: [ + { + ...options, + groups: [ + ['static-property', 'private-property', 'property'], + 'constructor', + ], + }, + ], + }, + ], + invalid: [ + { + code: dedent` + class Class { + [k: string]: any; + + [k: string]; + + static a = 'a'; + } + `, + output: dedent` + class Class { + static a = 'a'; + + [k: string]: any; + + [k: string]; + } + `, + options: [ + { + ...options, + groups: [ + ['static-property', 'private-property', 'property'], + 'constructor', + ], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: '[k: string];', + right: 'a', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): sorts class with ts index signatures`, + rule, + { + valid: [ + { + code: dedent` + class Decorations { + setBackground(color: number, hexFlag: boolean): this + setBackground(color: Color | string | CSSColor): this + setBackground(r: number, g: number, b: number, a?: number): this + setBackground(color: ColorArgument, arg1?: boolean | number, arg2?: number, arg3?: number): this { + /* ... */ + } + } + `, + options: [ + { + ...options, + groups: [ + ['static-property', 'private-property', 'property'], + 'constructor', + ], + }, + ], + }, + ], + invalid: [], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): sorts private methods with hash`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + class MyUnsortedClass { + someOtherProperty + + someProperty = 1 + + constructor() {} + + static #aPrivateStaticMethod () {} + + #somePrivateProperty + + #someOtherPrivateProperty = 2 + + static someStaticProperty = 3 + + static #someStaticPrivateProperty = 4 + + aInstanceMethod () {} + + static aStaticMethod () {} + + #aPrivateInstanceMethod () {} + } + `, + output: dedent` + class MyUnsortedClass { + static someStaticProperty = 3 + + #someOtherPrivateProperty = 2 + + #somePrivateProperty + + static #someStaticPrivateProperty = 4 + + someOtherProperty + + someProperty = 1 + + constructor() {} + + aInstanceMethod () {} + + #aPrivateInstanceMethod () {} + + static #aPrivateStaticMethod () {} + + static aStaticMethod () {} + } + `, + options: [ + { + ...options, + groups: [ + 'static-property', + 'private-property', + 'property', + 'constructor', + 'method', + 'private-method', + 'static-method', + 'unknown', + ], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: '#aPrivateStaticMethod', + right: '#somePrivateProperty', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: '#somePrivateProperty', + right: '#someOtherPrivateProperty', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: '#someOtherPrivateProperty', + right: 'someStaticProperty', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'aStaticMethod', + right: '#aPrivateInstanceMethod', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): allows split methods with getters and setters`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + class A { + x() {} + b() {} + get z() {} + get c() {} + set c() {} + } + `, + output: dedent` + class A { + b() {} + x() {} + get c() {} + set c() {} + get z() {} + } + `, + options: [ + { + ...options, + groups: [ + 'index-signature', + 'static-property', + 'private-property', + 'property', + 'constructor', + 'static-method', + 'private-method', + 'method', + ['get-method', 'set-method'], + 'unknown', + ], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'x', + right: 'b', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'z', + right: 'c', + }, + }, + ], + }, + ], + }, + ) + + ruleTester.run(`${ruleName}(${type}): sorts decorated properties`, rule, { + valid: [], + invalid: [ + { + code: dedent` + class User { + firstName: string + + id: number + + @Index({ name: 'born_index' }) + @Property() + born: string + + @Property() + @Unique() + email: string + + lastName: string + }`, + output: dedent` + class User { + firstName: string + + id: number + + lastName: string + + @Index({ name: 'born_index' }) + @Property() + born: string + + @Property() + @Unique() + email: string + }`, + options: [ + { + ...options, + groups: ['property', 'decorated-property', 'unknown'], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'email', + right: 'lastName', + }, + }, + ], + }, + { + code: dedent` + class MyElement { + @property({ attribute: false }) + data = {} + + @property() + greeting: string = 'Hello' + + @state() + private _counter = 0 + + private _message = '' + + private _prop = 0 + + constructor() {} + + @property() + get message(): string { + return this._message + } + + set message(message: string) { + this._message = message + } + + @property() + set prop(val: number) { + this._prop = Math.floor(val) + } + + get prop() { + return this._prop + } + + render() {} + }`, + output: dedent` + class MyElement { + @property({ attribute: false }) + data = {} + + @property() + greeting: string = 'Hello' + + @state() + private _counter = 0 + + private _message = '' + + private _prop = 0 + + constructor() {} + + @property() + get message(): string { + return this._message + } + + @property() + set prop(val: number) { + this._prop = Math.floor(val) + } + + set message(message: string) { + this._message = message + } + + get prop() { + return this._prop + } + + render() {} + }`, + options: [ + { + ...options, + groups: [ + 'decorated-property', + 'property', + 'private-decorated-property', + 'private-property', + 'constructor', + ['decorated-get-method', 'decorated-set-method'], + ['get-method', 'set-method'], + 'unknown', + ], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'message', + right: 'prop', + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${ruleName}(${type}): sorts decorated accessors`, rule, { + valid: [], + invalid: [ + { + code: dedent` + class Todo { + id = Math.random() + + constructor() {} + + @action + toggle() {} + + @observable + accessor #active = false + + @observable + accessor finished = false + + @observable + accessor title = '' + }`, + output: dedent` + class Todo { + @observable + accessor finished = false + + @observable + accessor title = '' + + @observable + accessor #active = false + + id = Math.random() + + constructor() {} + + @action + toggle() {} + }`, + options: [ + { + ...options, + groups: [ + 'decorated-accessor-property', + 'private-decorated-accessor-property', + 'property', + 'constructor', + 'decorated-method', + 'unknown', + ], + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'toggle', + right: '#active', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: '#active', + right: 'finished', + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${ruleName}(${type}): sorts decorated accessors`, rule, { + valid: [ + { + code: dedent` + class Class { + // Region: Table + protected onChangeColumns() {} + + protected onPaginationChanged() {} + + protected onSortChanged() {} + + protected updateTable() {} + + // Region: Form + protected clearForm() {} + + protected disableForm() {} + + // Regular Comment + protected onValueChanged() {} + + protected setFormValue() {} + } + `, + options: [ + { + ...options, + partitionByComment: 'Region:*', + }, + ], + }, + ], + invalid: [ + { + code: dedent` + class Class { + // Region: Table + protected onChangeColumns() {} + + protected updateTable() {} + + protected onSortChanged() {} + + protected onPaginationChanged() {} + + // Region: Form + protected clearForm() {} + + protected disableForm() {} + + protected setFormValue() {} + + // Regular Comment + protected onValueChanged() {} + } + `, + output: dedent` + class Class { + // Region: Table + protected onChangeColumns() {} + + protected onPaginationChanged() {} + + protected onSortChanged() {} + protected updateTable() {} - // Region: Form - protected clearForm() {} + // Region: Form + protected clearForm() {} + + protected disableForm() {} + + // Regular Comment + protected onValueChanged() {} + + protected setFormValue() {} + } + `, + options: [ + { + ...options, + partitionByComment: 'Region:*', + }, + ], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'updateTable', + right: 'onSortChanged', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'onSortChanged', + right: 'onPaginationChanged', + }, + }, + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'setFormValue', + right: 'onValueChanged', + }, + }, + ], + }, + ], + }) + + ruleTester.run( + `${ruleName}(${type}): does not sort properties if the right value depends on the left value`, + rule, + { + valid: [ + { + code: dedent` + class Class { + b = 'b' + + aaa = [this.b] + } + `, + options: [options], + }, + { + code: dedent` + class Class { + b = 'b' + + getAaa() { + return this.b; + } + } + `, + options: [options], + }, + { + code: dedent` + class Class { + static c = 'c' + + b = Example.c + } + `, + options: [options], + }, + { + code: dedent` + class Class { + #b = 'b' + + getAaa() { + return this.#b; + } + } + `, + options: [options], + }, + { + code: dedent` + class Class { + static b = 'b' + + static getAaa() { + return this.b; + } + } + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + class Class { + aaa = [this.b] + + b = 'b' + } + `, + output: dedent` + class Class { + b = 'b' + + aaa = [this.b] + } + `, + options: [options], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'aaa', + right: 'b', + }, + }, + ], + }, + { + code: dedent` + class Class { + getAaa() { + return this.b; + } + + b = 'b' + } + `, + output: dedent` + class Class { + b = 'b' + + getAaa() { + return this.b; + } + } + `, + options: [options], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'getAaa', + right: 'b', + }, + }, + ], + }, + { + code: dedent` + class Class { + b = Example.c + + static c = 'c' + } + `, + output: dedent` + class Class { + static c = 'c' - protected disableForm() {} + b = Example.c + } + `, + options: [options], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'b', + right: 'c', + }, + }, + ], + }, + { + code: dedent` + class Class { + getAaa() { + return this.#b; + } - // Regular Comment - protected onValueChanged() {} + #b = 'b' + } + `, + output: dedent` + class Class { + #b = 'b' - protected setFormValue() {} - } - `, - options: [ - { - ...options, - partitionByComment: 'Region:*', - }, - ], - }, - ], - invalid: [ - { - code: dedent` - export class Example { - // Region: Table - protected onChangeColumns() {} + getAaa() { + return this.#b; + } + } + `, + options: [options], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'getAaa', + right: '#b', + }, + }, + ], + }, + { + code: dedent` + class Class { + static getAaa() { + return this.b; + } - protected updateTable() {} + static b = 'b' + } + `, + output: dedent` + class Class { + static b = 'b' - protected onSortChanged() {} + static getAaa() { + return this.b; + } + } + `, + options: [options], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'getAaa', + right: 'b', + }, + }, + ], + }, + ], + }, + ) - protected onPaginationChanged() {} + ruleTester.run( + `${ruleName}(${type}): works with left and right dependencies`, + rule, + { + valid: [ + { + code: dedent` + class Class { + left = 'left' + right = 'right' - // Region: Form - protected clearForm() {} + aaa = this.left + this.right + } + `, + options: [options], + }, + { + code: dedent` + class Class { + condition1 = true + condition2 = false - protected disableForm() {} + result = this.condition1 && this.condition2 + } + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + class Class { + aaa = this.left + this.right - protected setFormValue() {} + left = 'left' - // Regular Comment - protected onValueChanged() {} - } - `, - output: dedent` - export class Example { - // Region: Table - protected onChangeColumns() {} + right = 'right' + } + `, + output: dedent` + class Class { + left = 'left' - protected onPaginationChanged() {} + right = 'right' - protected onSortChanged() {} + aaa = this.left + this.right + } + `, + options: [options], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'aaa', + right: 'left', + }, + }, + ], + }, + ], + }, + ) - protected updateTable() {} + ruleTester.run(`${ruleName}(${type}): works with body dependencies`, rule, { + valid: [ + { + code: dedent` + class Class { + a = 10 - // Region: Form - protected clearForm() {} + method = function() { + const b = this.a + 20; + return b; + } + } + `, + options: [options], + }, + { + code: dedent` + class Class { + a = 10 - protected disableForm() {} + method = () => { + const b = this.a + 20; + return b; + } + } + `, + options: [options], + }, + { + code: dedent` + class Class { + a = 10 + b = 20 + + method() { + { + const c = this.a + this.b; + console.log(c); + } + } + } + `, + options: [options], + }, + { + code: dedent` + class Class { + a = 10 + b = this.a + 20 - // Regular Comment - protected onValueChanged() {} + method() { + return this.b; + } + } + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + class Class { + method = function() { + const b = this.a + 20; + return b; + } - protected setFormValue() {} + a = 10 } `, - options: [ + output: dedent` + class Class { + a = 10 + + method = function() { + const b = this.a + 20; + return b; + } + } + `, + options: [options], + errors: [ { - ...options, - partitionByComment: 'Region:*', + messageId: 'unexpectedClassesOrder', + data: { + left: 'method', + right: 'a', + }, }, ], + }, + { + code: dedent` + class Class { + method = () => { + const b = this.a + 20; + return b; + } + + a = 10 + } + `, + output: dedent` + class Class { + a = 10 + + method = () => { + const b = this.a + 20; + return b; + } + } + `, + options: [options], errors: [ { messageId: 'unexpectedClassesOrder', data: { - left: 'updateTable', - right: 'onSortChanged', + left: 'method', + right: 'a', + }, + }, + ], + }, + { + code: dedent` + class Class { + method() { + { + const c = this.a + this.b; + console.log(c); + } + } + + a = 10 + + b = 20 + } + `, + output: dedent` + class Class { + a = 10 + + b = 20 + + method() { + { + const c = this.a + this.b; + console.log(c); + } + } + } + `, + options: [options], + errors: [ + { + messageId: 'unexpectedClassesOrder', + data: { + left: 'method', + right: 'a', }, }, + ], + }, + { + code: dedent` + class Class { + method() { + return this.b; + } + + b = this.a + 20 + + a = 10 + } + `, + output: dedent` + class Class { + a = 10 + + b = this.a + 20 + + method() { + return this.b; + } + } + `, + options: [options], + errors: [ { messageId: 'unexpectedClassesOrder', data: { - left: 'onSortChanged', - right: 'onPaginationChanged', + left: 'method', + right: 'b', }, }, { messageId: 'unexpectedClassesOrder', data: { - left: 'setFormValue', - right: 'onValueChanged', + left: 'b', + right: 'a', }, }, ],