"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.defaultOrder = void 0; const utils_1 = require("@typescript-eslint/utils"); const natural_compare_1 = __importDefault(require("natural-compare")); const util_1 = require("../util"); const neverConfig = { type: 'string', enum: ['never'], }; const arrayConfig = (memberTypes) => ({ type: 'array', items: { oneOf: [ { $ref: memberTypes, }, { type: 'array', items: { $ref: memberTypes, }, }, ], }, }); const objectConfig = (memberTypes) => ({ type: 'object', properties: { memberTypes: { oneOf: [arrayConfig(memberTypes), neverConfig], }, order: { $ref: '#/items/0/$defs/orderOptions', }, optionalityOrder: { $ref: '#/items/0/$defs/optionalityOrderOptions', }, }, additionalProperties: false, }); exports.defaultOrder = [ // Index signature 'signature', 'call-signature', // Fields 'public-static-field', 'protected-static-field', 'private-static-field', '#private-static-field', 'public-decorated-field', 'protected-decorated-field', 'private-decorated-field', 'public-instance-field', 'protected-instance-field', 'private-instance-field', '#private-instance-field', 'public-abstract-field', 'protected-abstract-field', 'public-field', 'protected-field', 'private-field', '#private-field', 'static-field', 'instance-field', 'abstract-field', 'decorated-field', 'field', // Static initialization 'static-initialization', // Constructors 'public-constructor', 'protected-constructor', 'private-constructor', 'constructor', // Accessors 'public-static-accessor', 'protected-static-accessor', 'private-static-accessor', '#private-static-accessor', 'public-decorated-accessor', 'protected-decorated-accessor', 'private-decorated-accessor', 'public-instance-accessor', 'protected-instance-accessor', 'private-instance-accessor', '#private-instance-accessor', 'public-abstract-accessor', 'protected-abstract-accessor', 'public-accessor', 'protected-accessor', 'private-accessor', '#private-accessor', 'static-accessor', 'instance-accessor', 'abstract-accessor', 'decorated-accessor', 'accessor', // Getters 'public-static-get', 'protected-static-get', 'private-static-get', '#private-static-get', 'public-decorated-get', 'protected-decorated-get', 'private-decorated-get', 'public-instance-get', 'protected-instance-get', 'private-instance-get', '#private-instance-get', 'public-abstract-get', 'protected-abstract-get', 'public-get', 'protected-get', 'private-get', '#private-get', 'static-get', 'instance-get', 'abstract-get', 'decorated-get', 'get', // Setters 'public-static-set', 'protected-static-set', 'private-static-set', '#private-static-set', 'public-decorated-set', 'protected-decorated-set', 'private-decorated-set', 'public-instance-set', 'protected-instance-set', 'private-instance-set', '#private-instance-set', 'public-abstract-set', 'protected-abstract-set', 'public-set', 'protected-set', 'private-set', '#private-set', 'static-set', 'instance-set', 'abstract-set', 'decorated-set', 'set', // Methods 'public-static-method', 'protected-static-method', 'private-static-method', '#private-static-method', 'public-decorated-method', 'protected-decorated-method', 'private-decorated-method', 'public-instance-method', 'protected-instance-method', 'private-instance-method', '#private-instance-method', 'public-abstract-method', 'protected-abstract-method', 'public-method', 'protected-method', 'private-method', '#private-method', 'static-method', 'instance-method', 'abstract-method', 'decorated-method', 'method', ]; const allMemberTypes = Array.from([ 'readonly-signature', 'signature', 'readonly-field', 'field', 'method', 'call-signature', 'constructor', 'accessor', 'get', 'set', 'static-initialization', ].reduce((all, type) => { all.add(type); ['public', 'protected', 'private', '#private'].forEach(accessibility => { if (type !== 'readonly-signature' && type !== 'signature' && type !== 'static-initialization' && type !== 'call-signature' && !(type === 'constructor' && accessibility === '#private')) { all.add(`${accessibility}-${type}`); // e.g. `public-field` } // Only class instance fields, methods, accessors, get and set can have decorators attached to them if (accessibility !== '#private' && (type === 'readonly-field' || type === 'field' || type === 'method' || type === 'accessor' || type === 'get' || type === 'set')) { all.add(`${accessibility}-decorated-${type}`); all.add(`decorated-${type}`); } if (type !== 'constructor' && type !== 'readonly-signature' && type !== 'signature' && type !== 'call-signature') { // There is no `static-constructor` or `instance-constructor` or `abstract-constructor` if (accessibility === '#private' || accessibility === 'private') { ['static', 'instance'].forEach(scope => { all.add(`${scope}-${type}`); all.add(`${accessibility}-${scope}-${type}`); }); } else { ['static', 'instance', 'abstract'].forEach(scope => { all.add(`${scope}-${type}`); all.add(`${accessibility}-${scope}-${type}`); }); } } }); return all; }, new Set())); const functionExpressions = [ utils_1.AST_NODE_TYPES.FunctionExpression, utils_1.AST_NODE_TYPES.ArrowFunctionExpression, ]; /** * Gets the node type. * * @param node the node to be evaluated. */ function getNodeType(node) { switch (node.type) { case utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition: case utils_1.AST_NODE_TYPES.MethodDefinition: return node.kind; case utils_1.AST_NODE_TYPES.TSMethodSignature: return 'method'; case utils_1.AST_NODE_TYPES.TSCallSignatureDeclaration: return 'call-signature'; case utils_1.AST_NODE_TYPES.TSConstructSignatureDeclaration: return 'constructor'; case utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition: return node.readonly ? 'readonly-field' : 'field'; case utils_1.AST_NODE_TYPES.AccessorProperty: return 'accessor'; case utils_1.AST_NODE_TYPES.PropertyDefinition: return node.value && functionExpressions.includes(node.value.type) ? 'method' : node.readonly ? 'readonly-field' : 'field'; case utils_1.AST_NODE_TYPES.TSPropertySignature: return node.readonly ? 'readonly-field' : 'field'; case utils_1.AST_NODE_TYPES.TSIndexSignature: return node.readonly ? 'readonly-signature' : 'signature'; case utils_1.AST_NODE_TYPES.StaticBlock: return 'static-initialization'; default: return null; } } /** * Gets the raw string value of a member's name */ function getMemberRawName(member, sourceCode) { const { name, type } = (0, util_1.getNameFromMember)(member, sourceCode); if (type === util_1.MemberNameType.Quoted) { return name.slice(1, -1); } if (type === util_1.MemberNameType.Private) { return name.slice(1); } return name; } /** * Gets the member name based on the member type. * * @param node the node to be evaluated. */ function getMemberName(node, sourceCode) { switch (node.type) { case utils_1.AST_NODE_TYPES.TSPropertySignature: case utils_1.AST_NODE_TYPES.TSMethodSignature: case utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition: case utils_1.AST_NODE_TYPES.PropertyDefinition: return getMemberRawName(node, sourceCode); case utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition: case utils_1.AST_NODE_TYPES.MethodDefinition: return node.kind === 'constructor' ? 'constructor' : getMemberRawName(node, sourceCode); case utils_1.AST_NODE_TYPES.TSConstructSignatureDeclaration: return 'new'; case utils_1.AST_NODE_TYPES.TSCallSignatureDeclaration: return 'call'; case utils_1.AST_NODE_TYPES.TSIndexSignature: return (0, util_1.getNameFromIndexSignature)(node); case utils_1.AST_NODE_TYPES.StaticBlock: return 'static block'; default: return null; } } /** * Returns true if the member is optional based on the member type. * * @param node the node to be evaluated. * * @returns Whether the member is optional, or false if it cannot be optional at all. */ function isMemberOptional(node) { switch (node.type) { case utils_1.AST_NODE_TYPES.TSPropertySignature: case utils_1.AST_NODE_TYPES.TSMethodSignature: case utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition: case utils_1.AST_NODE_TYPES.PropertyDefinition: case utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition: case utils_1.AST_NODE_TYPES.MethodDefinition: return !!node.optional; } return false; } /** * Gets the calculated rank using the provided method definition. * The algorithm is as follows: * - Get the rank based on the accessibility-scope-type name, e.g. public-instance-field * - If there is no order for accessibility-scope-type, then strip out the accessibility. * - If there is no order for scope-type, then strip out the scope. * - If there is no order for type, then return -1 * @param memberGroups the valid names to be validated. * @param orderConfig the current order to be validated. * * @return Index of the matching member type in the order configuration. */ function getRankOrder(memberGroups, orderConfig) { let rank = -1; const stack = memberGroups.slice(); // Get a copy of the member groups while (stack.length > 0 && rank === -1) { const memberGroup = stack.shift(); rank = orderConfig.findIndex(memberType => Array.isArray(memberType) ? memberType.includes(memberGroup) : memberType === memberGroup); } return rank; } function getAccessibility(node) { if ('accessibility' in node && node.accessibility) { return node.accessibility; } if ('key' in node && node.key.type === utils_1.AST_NODE_TYPES.PrivateIdentifier) { return '#private'; } return 'public'; } /** * Gets the rank of the node given the order. * @param node the node to be evaluated. * @param orderConfig the current order to be validated. * @param supportsModifiers a flag indicating whether the type supports modifiers (scope or accessibility) or not. */ function getRank(node, orderConfig, supportsModifiers) { const type = getNodeType(node); if (type == null) { // shouldn't happen but just in case, put it on the end return orderConfig.length - 1; } const abstract = node.type === utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition || node.type === utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition; const scope = 'static' in node && node.static ? 'static' : abstract ? 'abstract' : 'instance'; const accessibility = getAccessibility(node); // Collect all existing member groups that apply to this node... // (e.g. 'public-instance-field', 'instance-field', 'public-field', 'constructor' etc.) const memberGroups = []; if (supportsModifiers) { const decorated = 'decorators' in node && node.decorators.length > 0; if (decorated && (type === 'readonly-field' || type === 'field' || type === 'method' || type === 'get' || type === 'set')) { memberGroups.push(`${accessibility}-decorated-${type}`); memberGroups.push(`decorated-${type}`); if (type === 'readonly-field') { memberGroups.push(`${accessibility}-decorated-field`); memberGroups.push(`decorated-field`); } } if (type !== 'readonly-signature' && type !== 'signature' && type !== 'static-initialization') { if (type !== 'constructor') { // Constructors have no scope memberGroups.push(`${accessibility}-${scope}-${type}`); memberGroups.push(`${scope}-${type}`); if (type === 'readonly-field') { memberGroups.push(`${accessibility}-${scope}-field`); memberGroups.push(`${scope}-field`); } } memberGroups.push(`${accessibility}-${type}`); if (type === 'readonly-field') { memberGroups.push(`${accessibility}-field`); } } } memberGroups.push(type); if (type === 'readonly-signature') { memberGroups.push('signature'); } else if (type === 'readonly-field') { memberGroups.push('field'); } // ...then get the rank order for those member groups based on the node return getRankOrder(memberGroups, orderConfig); } /** * Gets the lowest possible rank(s) higher than target. * e.g. given the following order: * ... * public-static-method * protected-static-method * private-static-method * public-instance-method * protected-instance-method * private-instance-method * ... * and considering that a public-instance-method has already been declared, so ranks contains * public-instance-method, then the lowest possible rank for public-static-method is * public-instance-method. * If a lowest possible rank is a member group, a comma separated list of ranks is returned. * @param ranks the existing ranks in the object. * @param target the target rank. * @param order the current order to be validated. * @returns the name(s) of the lowest possible rank without dashes (-). */ function getLowestRank(ranks, target, order) { let lowest = ranks[ranks.length - 1]; ranks.forEach(rank => { if (rank > target) { lowest = Math.min(lowest, rank); } }); const lowestRank = order[lowest]; const lowestRanks = Array.isArray(lowestRank) ? lowestRank : [lowestRank]; return lowestRanks.map(rank => rank.replace(/-/g, ' ')).join(', '); } exports.default = (0, util_1.createRule)({ name: 'member-ordering', meta: { type: 'suggestion', docs: { description: 'Require a consistent member declaration order', }, messages: { incorrectOrder: 'Member {{member}} should be declared before member {{beforeMember}}.', incorrectGroupOrder: 'Member {{name}} should be declared before all {{rank}} definitions.', incorrectRequiredMembersOrder: `Member {{member}} should be declared after all {{optionalOrRequired}} members.`, }, schema: [ { $defs: { orderOptions: { type: 'string', enum: [ 'alphabetically', 'alphabetically-case-insensitive', 'as-written', 'natural', 'natural-case-insensitive', ], }, optionalityOrderOptions: { type: 'string', enum: ['optional-first', 'required-first'], }, allItems: { type: 'string', enum: allMemberTypes, }, typeItems: { type: 'string', enum: [ 'readonly-signature', 'signature', 'readonly-field', 'field', 'method', 'constructor', ], }, baseConfig: { oneOf: [ neverConfig, arrayConfig('#/items/0/$defs/allItems'), objectConfig('#/items/0/$defs/allItems'), ], }, typesConfig: { oneOf: [ neverConfig, arrayConfig('#/items/0/$defs/typeItems'), objectConfig('#/items/0/$defs/typeItems'), ], }, }, type: 'object', properties: { default: { $ref: '#/items/0/$defs/baseConfig', }, classes: { $ref: '#/items/0/$defs/baseConfig', }, classExpressions: { $ref: '#/items/0/$defs/baseConfig', }, interfaces: { $ref: '#/items/0/$defs/typesConfig', }, typeLiterals: { $ref: '#/items/0/$defs/typesConfig', }, }, additionalProperties: false, }, ], }, defaultOptions: [ { default: { memberTypes: exports.defaultOrder, }, }, ], create(context, [options]) { /** * Checks if the member groups are correctly sorted. * * @param members Members to be validated. * @param groupOrder Group order to be validated. * @param supportsModifiers A flag indicating whether the type supports modifiers (scope or accessibility) or not. * * @return Array of member groups or null if one of the groups is not correctly sorted. */ function checkGroupSort(members, groupOrder, supportsModifiers) { const previousRanks = []; const memberGroups = []; let isCorrectlySorted = true; // Find first member which isn't correctly sorted for (const member of members) { const rank = getRank(member, groupOrder, supportsModifiers); const name = getMemberName(member, context.sourceCode); const rankLastMember = previousRanks[previousRanks.length - 1]; if (rank === -1) { continue; } // Works for 1st item because x < undefined === false for any x (typeof string) if (rank < rankLastMember) { context.report({ node: member, messageId: 'incorrectGroupOrder', data: { name, rank: getLowestRank(previousRanks, rank, groupOrder), }, }); isCorrectlySorted = false; } else if (rank === rankLastMember) { // Same member group --> Push to existing member group array memberGroups[memberGroups.length - 1].push(member); } else { // New member group --> Create new member group array previousRanks.push(rank); memberGroups.push([member]); } } return isCorrectlySorted ? memberGroups : null; } /** * Checks if the members are alphabetically sorted. * * @param members Members to be validated. * @param order What order the members should be sorted in. * * @return True if all members are correctly sorted. */ function checkAlphaSort(members, order) { let previousName = ''; let isCorrectlySorted = true; // Find first member which isn't correctly sorted members.forEach(member => { const name = getMemberName(member, context.sourceCode); // Note: Not all members have names if (name) { if (naturalOutOfOrder(name, previousName, order)) { context.report({ node: member, messageId: 'incorrectOrder', data: { member: name, beforeMember: previousName, }, }); isCorrectlySorted = false; } previousName = name; } }); return isCorrectlySorted; } function naturalOutOfOrder(name, previousName, order) { if (name === previousName) { return false; } switch (order) { case 'alphabetically': return name < previousName; case 'alphabetically-case-insensitive': return name.toLowerCase() < previousName.toLowerCase(); case 'natural': return (0, natural_compare_1.default)(name, previousName) !== 1; case 'natural-case-insensitive': return ((0, natural_compare_1.default)(name.toLowerCase(), previousName.toLowerCase()) !== 1); } } /** * Checks if the order of optional and required members is correct based * on the given 'required' parameter. * * @param members Members to be validated. * @param optionalityOrder Where to place optional members, if not intermixed. * * @return True if all required and optional members are correctly sorted. */ function checkRequiredOrder(members, optionalityOrder) { const switchIndex = members.findIndex((member, i) => i && isMemberOptional(member) !== isMemberOptional(members[i - 1])); const report = (member) => context.report({ messageId: 'incorrectRequiredMembersOrder', loc: member.loc, data: { member: getMemberName(member, context.sourceCode), optionalOrRequired: optionalityOrder === 'required-first' ? 'required' : 'optional', }, }); // if the optionality of the first item is correct (based on optionalityOrder) // then the first 0 inclusive to switchIndex exclusive members all // have the correct optionality if (isMemberOptional(members[0]) !== (optionalityOrder === 'optional-first')) { report(members[0]); return false; } for (let i = switchIndex + 1; i < members.length; i++) { if (isMemberOptional(members[i]) !== isMemberOptional(members[switchIndex])) { report(members[switchIndex]); return false; } } return true; } /** * Validates if all members are correctly sorted. * * @param members Members to be validated. * @param orderConfig Order config to be validated. * @param supportsModifiers A flag indicating whether the type supports modifiers (scope or accessibility) or not. */ function validateMembersOrder(members, orderConfig, supportsModifiers) { if (orderConfig === 'never') { return; } // Standardize config let order; let memberTypes; let optionalityOrder; // returns true if everything is good and false if an error was reported const checkOrder = (memberSet) => { const hasAlphaSort = !!(order && order !== 'as-written'); // Check order if (Array.isArray(memberTypes)) { const grouped = checkGroupSort(memberSet, memberTypes, supportsModifiers); if (grouped == null) { return false; } if (hasAlphaSort) { return !grouped.some(groupMember => !checkAlphaSort(groupMember, order)); } } else if (hasAlphaSort) { return checkAlphaSort(memberSet, order); } return true; }; if (Array.isArray(orderConfig)) { memberTypes = orderConfig; } else { order = orderConfig.order; memberTypes = orderConfig.memberTypes; optionalityOrder = orderConfig.optionalityOrder; } if (!optionalityOrder) { checkOrder(members); return; } const switchIndex = members.findIndex((member, i) => i && isMemberOptional(member) !== isMemberOptional(members[i - 1])); if (switchIndex !== -1) { if (!checkRequiredOrder(members, optionalityOrder)) { return; } checkOrder(members.slice(0, switchIndex)); checkOrder(members.slice(switchIndex)); } else { checkOrder(members); } } return { ClassDeclaration(node) { validateMembersOrder(node.body.body, options.classes ?? options.default, true); }, ClassExpression(node) { validateMembersOrder(node.body.body, options.classExpressions ?? options.default, true); }, TSInterfaceDeclaration(node) { validateMembersOrder(node.body.body, options.interfaces ?? options.default, false); }, TSTypeLiteral(node) { validateMembersOrder(node.members, options.typeLiterals ?? options.default, false); }, }; }, }); //# sourceMappingURL=member-ordering.js.map