diff --git a/lib/utils/ember.js b/lib/utils/ember.js index 45b934ca09..06b68bccaf 100644 --- a/lib/utils/ember.js +++ b/lib/utils/ember.js @@ -31,7 +31,10 @@ module.exports = { isEmberHelper, isEmberProxy, + isActionMethod, + isSingleLineAccessor, isSingleLineFn, + isMultiLineAccessor, isMultiLineFn, isFunctionExpression, @@ -478,10 +481,12 @@ function isObjectProp(node) { return types.isObjectExpression(node.value); } -function isCustomProp(property) { - const value = property.value; +function isCustomProp(property, isNativeClass = false) { + const { value } = property; if (!value) { - return false; + // Native classes allow for empty values + // e.g. class Example { foo; } + return isNativeClass; } const isCustomObjectProp = types.isObjectExpression(value) && property.key.name !== 'actions'; @@ -509,6 +514,13 @@ function isActionsProp(property) { ); } +function isActionMethod(property) { + if (property?.type === 'MethodDefinition') { + return decoratorUtils.hasDecorator(property, 'action'); + } + return false; +} + function isComponentLifecycleHookName(name) { return [ 'didDestroyElement', @@ -635,6 +647,13 @@ function getEmberImportAliasName(importDeclaration) { return importDeclaration.specifiers[0].local.name; } +function isSingleLineAccessor(property) { + return ( + (types.isPropAccessor(property) || decoratorUtils.hasDecorator(property, 'computed')) && + utils.getSize(property) === 1 + ); +} + function isSingleLineFn(property, importedEmberName, importedObserverName) { return ( (types.isMethodDefinition(property) && utils.getSize(property) === 1) || @@ -647,6 +666,13 @@ function isSingleLineFn(property, importedEmberName, importedObserverName) { ); } +function isMultiLineAccessor(property) { + return ( + (types.isPropAccessor(property) || decoratorUtils.hasDecorator(property, 'computed')) && + utils.getSize(property) > 1 + ); +} + function isMultiLineFn(property, importedEmberName, importedObserverName) { return ( (types.isMethodDefinition(property) && utils.getSize(property) > 1) || @@ -663,7 +689,8 @@ function isFunctionExpression(property) { return ( types.isFunctionExpression(property) || types.isArrowFunctionExpression(property) || - types.isCallWithFunctionExpression(property) + types.isCallWithFunctionExpression(property) || + types.isCallWithArrowFunctionExpression(property) ); } diff --git a/lib/utils/property-order.js b/lib/utils/property-order.js index a01c471d4a..e8e9a6f452 100644 --- a/lib/utils/property-order.js +++ b/lib/utils/property-order.js @@ -7,6 +7,7 @@ const decoratorUtils = require('../utils/decorators'); module.exports = { determinePropertyType, + determinePropertyTypeInNativeClass, reportUnorderedProperties, addBackwardsPosition, }; @@ -148,6 +149,95 @@ function determinePropertyType( return 'unknown'; } +function determinePropertyTypeInNativeClass( + node, + parentType, + ORDER, + importedEmberName, + importedInjectName, + importedObserverName, + importedControllerName +) { + if (node === undefined) { + return 'unknown'; + } + + if (ember.isInjectedServiceProp(node, importedEmberName, importedInjectName)) { + return 'service'; + } + + if (ember.isInjectedControllerProp(node, importedEmberName, importedControllerName)) { + return 'controller'; + } + + if (node.type === 'MethodDefinition' && node.key.name === 'constructor') { + return 'constructor'; + } + + if (parentType === 'component') { + if (node.type === 'MethodDefinition' && ember.isComponentLifecycleHook(node)) { + return node.key.name; + } + } else if (parentType === 'controller') { + if ( + node.key !== undefined && + node.key.type === 'Identifier' && + node.key.name === 'queryParams' + ) { + return 'query-params'; + } else if (ember.isControllerDefaultProp(node)) { + return 'inherited-property'; + } + } else if (parentType === 'model') { + if (decoratorUtils.isClassPropertyOrPropertyDefinitionWithDecorator(node, 'attr')) { + return 'attribute'; + } else if (ember.isRelation(node)) { + return 'relationship'; + } + } else if (parentType === 'route') { + if (ember.isRouteDefaultProp(node)) { + return 'inherited-property'; + } else if (ember.isRouteLifecycleHook(node)) { + return node.key.name; + } + } + + if (parentType !== 'model' && ember.isActionMethod(node)) { + return 'action'; + } + + if (ember.isSingleLineAccessor(node)) { + return 'single-line-accessor'; + } + + if (ember.isMultiLineAccessor(node)) { + return 'multi-line-accessor'; + } + + const propName = getNodeKeyName(node); + const possibleOrderName = `custom:${propName}`; + if (ORDER.includes(possibleOrderName)) { + return possibleOrderName; + } + + if ( + (node.type === 'ClassProperty' || node.type === 'PropertyDefinition') && + ember.isCustomProp(node, true) + ) { + return 'property'; + } + + if (node.value && ember.isFunctionExpression(node.value)) { + if (utils.isEmptyMethod(node)) { + return 'empty-method'; + } + + return 'method'; + } + + return 'unknown'; +} + function getOrder(ORDER, type) { for (let i = 0, len = ORDER.length; i < len; i++) { const value = ORDER[i]; diff --git a/lib/utils/types.js b/lib/utils/types.js index ba73c1a948..4174c28c07 100644 --- a/lib/utils/types.js +++ b/lib/utils/types.js @@ -14,6 +14,7 @@ module.exports = { isAssignmentExpression, isBinaryExpression, isCallExpression, + isCallWithArrowFunctionExpression, isCallWithFunctionExpression, isClassDeclaration, isClassPropertyOrPropertyDefinition, @@ -36,6 +37,7 @@ module.exports = { isObjectPattern, isOptionalCallExpression, isOptionalMemberExpression, + isPropAccessor, isProperty, isReturnStatement, isSpreadElement, @@ -113,6 +115,36 @@ function isCallExpression(node) { return node !== undefined && node.type === 'CallExpression'; } +/** + * Check whether or not a node is a CallExpression that has a + * ArrowExpression as the first argument + * + * @example + * ```js + * // Native class + * tSomeAction = mysteriousFnc(() => {}) + * + * // Classic Ember object + * tSomeAction: mysteriousFnc(() => {}) + * ``` + * + * @param {Object} node The node to check + * @return {boolean} Whether or not the node is a call with a arrow function expression as the first argument + */ +function isCallWithArrowFunctionExpression(node) { + if (!node?.type === 'CallExpression') { + return false; + } + const callObj = node.callee?.type === 'MemberExpression' ? node.callee.object : node; + const firstArg = callObj.arguments ? callObj.arguments[0] : null; + return ( + callObj !== undefined && + callObj?.type === 'CallExpression' && + firstArg && + firstArg?.type === 'ArrowFunctionExpression' + ); +} + /** * Check whether or not a node is a CallExpression that has a FunctionExpression * as first argument, eg.: @@ -358,6 +390,16 @@ function isOptionalMemberExpression(node) { return node.type === 'OptionalMemberExpression'; } +/** + * Check whether a node is a property accessor (get/set). + * + * @param {Node} node the node to check + * @returns {boolean} whether the node is an accessor + */ +function isPropAccessor(node) { + return node?.type === 'MethodDefinition' && ['get', 'set'].includes(node?.kind); +} + /** * Check whether or not a node is an Property. * diff --git a/tests/lib/utils/ember-test.js b/tests/lib/utils/ember-test.js index 0f1c643e6c..139ef0782e 100644 --- a/tests/lib/utils/ember-test.js +++ b/tests/lib/utils/ember-test.js @@ -5,7 +5,13 @@ const emberUtils = require('../../../lib/utils/ember'); const { FauxContext } = require('../../helpers/faux-context'); function parse(code) { - return babelESLintParse(code).body[0].expression; + const { body } = babelESLintParse(code); + const [firstBodyNode] = body; + if (firstBodyNode.type === 'ClassDeclaration') { + // return first node within ClassBody + return firstBodyNode.body.body[0]; + } + return firstBodyNode.expression; } function getProperty(code) { @@ -320,7 +326,7 @@ describe('isEmberComponent', () => { }); }); -describe('isGlimerComponent', () => { +describe('isGlimmerComponent', () => { describe("should check if it's a Glimmer Component", () => { it('should detect Component when using native classes', () => { const context = new FauxContext(` @@ -1475,6 +1481,18 @@ describe('isActionsProp', () => { }); }); +describe('isActionMethod', () => { + const node = parse( + `class Test { + @action foo() {} + }` + ); + + it('should be actions method', () => { + expect(emberUtils.isActionMethod(node)).toBeTruthy(); + }); +}); + describe('getModuleProperties', () => { it("returns module's properties", () => { const code = ` @@ -1535,6 +1553,18 @@ describe('getModuleProperties', () => { }); }); +describe('isSingleLineAccessor', () => { + const node = parse( + `class Test { + get foo() { return 'bar'; } + }` + ); + + it('should be single line accessor', () => { + expect(emberUtils.isSingleLineAccessor(node)).toBeTruthy(); + }); +}); + describe('isSingleLineFn', () => { const property = getProperty(`test = { test: computed.or('asd', 'qwe') @@ -1563,6 +1593,20 @@ describe('isSingleLineFn', () => { }); }); +describe('isMultiLineAccessor', () => { + const node = parse( + `class Test { + get foo() { + return 'bar'; + } + }` + ); + + it('should be multi line accessor', () => { + expect(emberUtils.isMultiLineAccessor(node)).toBeTruthy(); + }); +}); + describe('isMultiLineFn', () => { const property = getProperty(`test = { test: computed('asd', function() { @@ -1616,6 +1660,11 @@ describe('isFunctionExpression', () => { test: () => {} }`); expect(emberUtils.isFunctionExpression(property.value)).toBeTruthy(); + + property = getProperty(`test = { + test: someFn(() => {}) + }`); + expect(emberUtils.isFunctionExpression(property.value)).toBeTruthy(); }); }); diff --git a/tests/lib/utils/property-order-test.js b/tests/lib/utils/property-order-test.js index 3258195e9a..0da9bc6ff4 100644 --- a/tests/lib/utils/property-order-test.js +++ b/tests/lib/utils/property-order-test.js @@ -302,138 +302,233 @@ describe('determinePropertyType', () => { ); }); }); +}); - describe('native classes', () => { - it('should determine service-type props', () => { - const context = new FauxContext( - `import {inject as service} from '@ember/service'; - class MyController extends Controller { - @service currentUser; - }` - ); - const importInjectName = context.ast.body[0].specifiers[0].local.name; - const node = context.ast.body[1].body.body[0]; - expect( - propertyOrder.determinePropertyType(node, 'controller', [], undefined, importInjectName) - ).toBe('service'); - }); +describe('determinePropertyTypeInNativeClass', () => { + it('should determine service-type props', () => { + const context = new FauxContext( + `import {inject as service} from '@ember/service'; + class MyController extends Controller { + @service currentUser; + }` + ); + const importInjectName = context.ast.body[0].specifiers[0].local.name; + const node = context.ast.body[1].body.body[0]; + expect( + propertyOrder.determinePropertyTypeInNativeClass( + node, + 'controller', + [], + undefined, + importInjectName + ) + ).toBe('service'); + }); + it('should determine controller-type props', () => { + const context = new FauxContext( + `import {inject as controller} from '@ember/controller'; + class MyController extends Controller { + @controller application; + }` + ); + const importControllerName = context.ast.body[0].specifiers[0].local.name; + const node = context.ast.body[1].body.body[0]; + expect( + propertyOrder.determinePropertyTypeInNativeClass( + node, + 'controller', + [], + undefined, + undefined, + undefined, + importControllerName + ) + ).toBe('controller'); + }); - it('should determine controller-type props', () => { - const context = new FauxContext( - `import {inject as controller} from '@ember/controller'; - class MyController extends Controller { - @controller application; + it('should determine constructor-type props', () => { + const context = new FauxContext( + `class MyController extends Controller { + constructor() {} }` - ); - const importControllerName = context.ast.body[0].specifiers[0].local.name; - const node = context.ast.body[1].body.body[0]; - expect( - propertyOrder.determinePropertyType( - node, - 'controller', - [], - undefined, - undefined, - undefined, - importControllerName - ) - ).toBe('controller'); - }); + ); + const node = context.ast.body[0].body.body[0]; + expect(propertyOrder.determinePropertyTypeInNativeClass(node, 'controller')).toBe( + 'constructor' + ); + }); - it('should determine init-type props', () => { - const context = new FauxContext( - `class MyController extends Controller { - init() {} + it('should determine component lifecycle hooks', () => { + const context = new FauxContext( + `class MyComponent extends Component { + willDestroy() {} }` - ); - const node = context.ast.body[0].body.body[0]; - expect(propertyOrder.determinePropertyType(node, 'controller')).toBe('init'); - }); + ); + const node = context.ast.body[0].body.body[0]; + expect(propertyOrder.determinePropertyTypeInNativeClass(node, 'component', [])).toBe( + 'willDestroy' + ); + }); - it('should determine query-params', () => { - const context = new FauxContext( - `class MyController extends Controller { - queryParams = []; - }` - ); - const node = context.ast.body[0].body.body[0]; - expect(propertyOrder.determinePropertyType(node, 'controller')).toBe('query-params'); - }); + it('should determine query-params', () => { + const context = new FauxContext( + `class MyController extends Controller { + queryParams = []; + }` + ); + const node = context.ast.body[0].body.body[0]; + expect(propertyOrder.determinePropertyTypeInNativeClass(node, 'controller')).toBe( + 'query-params' + ); + }); + it('should determine attributes', () => { + const context = new FauxContext( + `class MyModel extends Model { + @attr someAttr; + }` + ); + const node = context.ast.body[0].body.body[0]; + expect(propertyOrder.determinePropertyTypeInNativeClass(node, 'model')).toBe('attribute'); + }); + it('should determine relationships', () => { + const context = new FauxContext( + `class MyModel extends Model { + @hasMany('otherModel') someRelationship; + }` + ); + const node = context.ast.body[0].body.body[0]; + expect(propertyOrder.determinePropertyTypeInNativeClass(node, 'model')).toBe('relationship'); + }); - it('should determine attributes', () => { - const context = new FauxContext( - `class MyModel extends Model { - @attr someAttr; - }` - ); - const node = context.ast.body[0].body.body[0]; - expect(propertyOrder.determinePropertyType(node, 'model')).toBe('attribute'); - }); + it('should determine single-line accessor', () => { + const context = new FauxContext( + `class MyComponent extends Component { + get myProp() {}; + }` + ); + const node = context.ast.body[0].body.body[0]; + expect(propertyOrder.determinePropertyTypeInNativeClass(node, 'component', [])).toBe( + 'single-line-accessor' + ); + }); - it('should determine relationships', () => { - const context = new FauxContext( - `class MyModel extends Model { - @hasMany('otherModel') someRelationship; + it('should determine multi-line accessor', () => { + const context = new FauxContext( + `class MyComponent extends Component { + get myProp() { + console.log('bar'); + }; }` - ); - const node = context.ast.body[0].body.body[0]; - expect(propertyOrder.determinePropertyType(node, 'model')).toBe('relationship'); - }); + ); + const node = context.ast.body[0].body.body[0]; + expect(propertyOrder.determinePropertyTypeInNativeClass(node, 'component', [])).toBe( + 'multi-line-accessor' + ); + }); - it('should determine observer-type props', () => { - const context = new FauxContext( - `import {observer} from '@ember/object'; - class MyController extends Controller { - @observer someObvs; - }` - ); - const importObserverName = context.ast.body[0].specifiers[0].local.name; - const node = context.ast.body[1].body.body[0]; - expect( - propertyOrder.determinePropertyType( - node, - 'controller', - [], - undefined, - undefined, - importObserverName - ) - ).toBe('observer'); - }); + it('should determine single-line functions', () => { + const context = new FauxContext( + `class MyComponent extends Component { + @computed get myProp() {}; + }` + ); + const node = context.ast.body[0].body.body[0]; + expect(propertyOrder.determinePropertyTypeInNativeClass(node, 'component', [])).toBe( + 'single-line-accessor' + ); + }); - it('should determine single-line functions', () => { - const context = new FauxContext( - `class MyComponent extends Component { - foo() {} - }` - ); - const node = context.ast.body[0].body.body[0]; - expect(propertyOrder.determinePropertyType(node, 'component')).toBe('single-line-function'); - }); + it('should determine multi-line functions', () => { + const context = new FauxContext( + `class MyComponent extends Component { + @computed + get myProp() { + console.log('bar'); + }; + }` + ); + const node = context.ast.body[0].body.body[0]; + expect(propertyOrder.determinePropertyTypeInNativeClass(node, 'component', [])).toBe( + 'multi-line-accessor' + ); + }); - it('should determine multi-line functions', () => { - const context = new FauxContext( - `class MyComponent extends Component { - foo(bar) { - console.log(bar) - } - }` - ); - const node = context.ast.body[0].body.body[0]; - expect(propertyOrder.determinePropertyType(node, 'component', [])).toBe( - 'multi-line-function' - ); - }); + it('should determine actions', () => { + const context = new FauxContext( + `import {action} from '@ember/object'; + class MyComponent extends Component { + @action fooAction() {}; + }` + ); + const node = context.ast.body[1].body.body[0]; + expect(propertyOrder.determinePropertyTypeInNativeClass(node, 'component')).toBe('action'); + }); - it('should determine properties', () => { - const context = new FauxContext( - `class MyComponent extends Component { - foo = "boo"; - }` - ); - const node = context.ast.body[0].body.body[0]; - expect(propertyOrder.determinePropertyType(node, 'component', [])).toBe('property'); - }); + it('should determine empty methods', () => { + const context = new FauxContext( + `class MyComponent extends Component { + foo() {} + }` + ); + const node = context.ast.body[0].body.body[0]; + expect(propertyOrder.determinePropertyTypeInNativeClass(node, 'component', [])).toBe( + 'empty-method' + ); + }); + + it('should determine methods', () => { + let context = new FauxContext( + `class MyComponent extends Component { + foo(bar) { + console.log(bar) + } + }` + ); + let node = context.ast.body[0].body.body[0]; + expect(propertyOrder.determinePropertyTypeInNativeClass(node, 'component', [])).toBe('method'); + + context = new FauxContext( + `class MyComponent extends Component { + fooTask = task(async () => { + console.log('foo'); + }) + }` + ); + node = context.ast.body[0].body.body[0]; + expect(propertyOrder.determinePropertyTypeInNativeClass(node, 'component', [])).toBe('method'); + + context = new FauxContext( + `class trackedMyComponent extends Component { + @task + fooTask2() { + console.log('foo'); + } + }` + ); + node = context.ast.body[0].body.body[0]; + expect(propertyOrder.determinePropertyTypeInNativeClass(node, 'component', [])).toBe('method'); + }); + + it('should determine properties', () => { + let context = new FauxContext( + `class MyComponent extends Component { + foo = "boo"; + }` + ); + let node = context.ast.body[0].body.body[0]; + expect(propertyOrder.determinePropertyTypeInNativeClass(node, 'component', [])).toBe( + 'property' + ); + + context = new FauxContext( + `class MyComponent extends Component { + foo; + }` + ); + node = context.ast.body[0].body.body[0]; + expect(propertyOrder.determinePropertyTypeInNativeClass(node, 'component', [])).toBe( + 'property' + ); }); }); diff --git a/tests/lib/utils/types-test.js b/tests/lib/utils/types-test.js index d60f28306d..dccdb3ab81 100644 --- a/tests/lib/utils/types-test.js +++ b/tests/lib/utils/types-test.js @@ -2,7 +2,13 @@ const { parse: babelESLintParse } = require('../../helpers/babel-eslint-parser') const types = require('../../../lib/utils/types'); function parse(code) { - return babelESLintParse(code).body[0].expression; + const { body } = babelESLintParse(code); + const [firstBodyNode] = body; + if (firstBodyNode.type === 'ClassDeclaration') { + // return first node within ClassBody + return firstBodyNode.body.body[0]; + } + return firstBodyNode.expression; } describe('function sort order', function () { @@ -45,6 +51,14 @@ describe('isCallWithFunctionExpression', () => { }); }); +describe('isCallWithArrowFunctionExpression', () => { + const node = parse('mysteriousFnc(() => {})'); + + it('should check if node is call with function expression', () => { + expect(types.isCallWithArrowFunctionExpression(node)).toBeTruthy(); + }); +}); + describe('isConciseArrowFunctionExpressionWithCall', () => { const node = parse('test = () => foo()').right; const blockNode = parse('test = () => { foo() }').right; @@ -114,6 +128,35 @@ describe('isObjectExpression', () => { }); }); +describe('isPropAccessor', () => { + it('should check if node is a getter', () => { + const node = parse( + `class Test { + get fooProp() {} + }` + ); + expect(types.isPropAccessor(node)).toBeTruthy(); + }); + + it('should check if node is a setter', () => { + const node = parse( + `class Test { + set fooProp(bar) {} + }` + ); + expect(types.isPropAccessor(node)).toBeTruthy(); + }); + + it('should check if node is a not a getter/setter', () => { + const node = parse( + `class Test { + fooProp() {} + }` + ); + expect(types.isPropAccessor(node)).toBeFalsy(); + }); +}); + describe('isReturnStatement', () => { const node = babelESLintParse('return').body[0];