diff options
Diffstat (limited to 'node_modules/tslint/lib/rules/memberOrderingRule.js')
-rw-r--r-- | node_modules/tslint/lib/rules/memberOrderingRule.js | 204 |
1 files changed, 190 insertions, 14 deletions
diff --git a/node_modules/tslint/lib/rules/memberOrderingRule.js b/node_modules/tslint/lib/rules/memberOrderingRule.js index e2e35a548..a9752e3a4 100644 --- a/node_modules/tslint/lib/rules/memberOrderingRule.js +++ b/node_modules/tslint/lib/rules/memberOrderingRule.js @@ -17,6 +17,7 @@ */ Object.defineProperty(exports, "__esModule", { value: true }); var tslib_1 = require("tslib"); +var tsutils_1 = require("tsutils"); var ts = require("typescript"); var error_1 = require("../error"); var Lint = require("../index"); @@ -96,7 +97,7 @@ var allMemberKindNames = utils_1.mapDefined(Object.keys(MemberKind), function (k function namesMarkdown(names) { return names.map(function (name) { return "* `" + name + "`"; }).join("\n "); } -var optionsDescription = (_a = ["\n One argument, which is an object, must be provided. It should contain an `order` property.\n The `order` property should have a value of one of the following strings:\n\n ", "\n\n Alternatively, the value for `order` maybe be an array consisting of the following strings:\n\n ", "\n\n You can also omit the access modifier to refer to \"public-\", \"protected-\", and \"private-\" all at once; for example, \"static-field\".\n\n You can also make your own categories by using an object instead of a string:\n\n {\n \"name\": \"static non-private\",\n \"kinds\": [\n \"public-static-field\",\n \"protected-static-field\",\n \"public-static-method\",\n \"protected-static-method\"\n ]\n }\n\n The '", "' option will enforce that members within the same category should be alphabetically sorted by name."], _a.raw = ["\n One argument, which is an object, must be provided. It should contain an \\`order\\` property.\n The \\`order\\` property should have a value of one of the following strings:\n\n ", "\n\n Alternatively, the value for \\`order\\` maybe be an array consisting of the following strings:\n\n ", "\n\n You can also omit the access modifier to refer to \"public-\", \"protected-\", and \"private-\" all at once; for example, \"static-field\".\n\n You can also make your own categories by using an object instead of a string:\n\n {\n \"name\": \"static non-private\",\n \"kinds\": [\n \"public-static-field\",\n \"protected-static-field\",\n \"public-static-method\",\n \"protected-static-method\"\n ]\n }\n\n The '", "' option will enforce that members within the same category should be alphabetically sorted by name."], Lint.Utils.dedent(_a, namesMarkdown(PRESET_NAMES), namesMarkdown(allMemberKindNames), OPTION_ALPHABETIZE)); +var optionsDescription = Lint.Utils.dedent(templateObject_1 || (templateObject_1 = tslib_1.__makeTemplateObject(["\n One argument, which is an object, must be provided. It should contain an `order` property.\n The `order` property should have a value of one of the following strings:\n\n ", "\n\n Alternatively, the value for `order` maybe be an array consisting of the following strings:\n\n ", "\n\n You can also omit the access modifier to refer to \"public-\", \"protected-\", and \"private-\" all at once; for example, \"static-field\".\n\n You can also make your own categories by using an object instead of a string:\n\n {\n \"name\": \"static non-private\",\n \"kinds\": [\n \"public-static-field\",\n \"protected-static-field\",\n \"public-static-method\",\n \"protected-static-method\"\n ]\n }\n\n The '", "' option will enforce that members within the same category should be alphabetically sorted by name."], ["\n One argument, which is an object, must be provided. It should contain an \\`order\\` property.\n The \\`order\\` property should have a value of one of the following strings:\n\n ", "\n\n Alternatively, the value for \\`order\\` maybe be an array consisting of the following strings:\n\n ", "\n\n You can also omit the access modifier to refer to \"public-\", \"protected-\", and \"private-\" all at once; for example, \"static-field\".\n\n You can also make your own categories by using an object instead of a string:\n\n {\n \"name\": \"static non-private\",\n \"kinds\": [\n \"public-static-field\",\n \"protected-static-field\",\n \"public-static-method\",\n \"protected-static-method\"\n ]\n }\n\n The '", "' option will enforce that members within the same category should be alphabetically sorted by name."])), namesMarkdown(PRESET_NAMES), namesMarkdown(allMemberKindNames), OPTION_ALPHABETIZE); var Rule = /** @class */ (function (_super) { tslib_1.__extends(Rule, _super); function Rule() { @@ -124,7 +125,8 @@ var Rule = /** @class */ (function (_super) { Rule.metadata = { ruleName: "member-ordering", description: "Enforces member ordering.", - rationale: "A consistent ordering for class members can make classes easier to read, navigate, and edit.", + hasFix: true, + rationale: Lint.Utils.dedent(templateObject_2 || (templateObject_2 = tslib_1.__makeTemplateObject(["\n A consistent ordering for class members can make classes easier to read, navigate, and edit.\n\n A common opposite practice to `member-ordering` is to keep related groups of classes together.\n Instead of creating classes with multiple separate groups, consider splitting class responsibilities\n apart across multiple single-responsibility classes.\n "], ["\n A consistent ordering for class members can make classes easier to read, navigate, and edit.\n\n A common opposite practice to \\`member-ordering\\` is to keep related groups of classes together.\n Instead of creating classes with multiple separate groups, consider splitting class responsibilities\n apart across multiple single-responsibility classes.\n "]))), optionsDescription: optionsDescription, options: { type: "object", @@ -187,11 +189,15 @@ exports.Rule = Rule; var MemberOrderingWalker = /** @class */ (function (_super) { tslib_1.__extends(MemberOrderingWalker, _super); function MemberOrderingWalker() { - return _super !== null && _super.apply(this, arguments) || this; + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.fixes = []; + return _this; } MemberOrderingWalker.prototype.walk = function (sourceFile) { var _this = this; var cb = function (node) { + // NB: iterate through children first! + ts.forEachChild(node, cb); switch (node.kind) { case ts.SyntaxKind.ClassDeclaration: case ts.SyntaxKind.ClassExpression: @@ -199,13 +205,27 @@ var MemberOrderingWalker = /** @class */ (function (_super) { case ts.SyntaxKind.TypeLiteral: _this.checkMembers(node.members); } - return ts.forEachChild(node, cb); }; - return ts.forEachChild(sourceFile, cb); + ts.forEachChild(sourceFile, cb); + // assign Replacements which have not been merged into surrounding ones to their RuleFailures. + this.fixes.forEach(function (_a) { + var failure = _a[0], replacement = _a[1]; + failure.getFix().push(replacement); + }); }; + /** + * Check wether the passed members adhere to the configured order. If not, RuleFailures are generated and a single + * Lint.Replacement is generated, which replaces the entire NodeArray with a correctly sorted one. The Replacement + * is not immediately added to a RuleFailure, as incorrectly sorted nodes can be nested (e.g. a class declaration + * in a method implementation), but instead temporarily stored in `this.fixes`. Nested Replacements are manually + * merged, as TSLint doesn't handle overlapping ones. For this reason it is important that the recursion happens + * before the checkMembers call in this.walk(). + */ MemberOrderingWalker.prototype.checkMembers = function (members) { + var _this = this; var prevRank = -1; var prevName; + var failureExists = false; for (var _i = 0, members_1 = members; _i < members_1.length; _i++) { var member = members_1[_i]; var rank = this.memberRank(member); @@ -222,7 +242,9 @@ var MemberOrderingWalker = /** @class */ (function (_super) { : "at the beginning of the class/interface"; var errorLine1 = "Declaration of " + nodeType + " not allowed after declaration of " + prevNodeType + ". " + ("Instead, this should come " + locationHint + "."); - this.addFailureAtNode(member, errorLine1); + // add empty array as fix so we can add a replacement later. (fix itself is readonly) + this.addFailureAtNode(member, errorLine1, []); + failureExists = true; } else { if (this.options.alphabetize && member.name !== undefined) { @@ -232,7 +254,8 @@ var MemberOrderingWalker = /** @class */ (function (_super) { } var curName = nameString(member.name); if (prevName !== undefined && caseInsensitiveLess(curName, prevName)) { - this.addFailureAtNode(member.name, Rule.FAILURE_STRING_ALPHABETIZE(this.findLowerName(members, rank, curName), curName)); + this.addFailureAtNode(member.name, Rule.FAILURE_STRING_ALPHABETIZE(this.findLowerName(members, rank, curName), curName), []); + failureExists = true; } else { prevName = curName; @@ -242,6 +265,56 @@ var MemberOrderingWalker = /** @class */ (function (_super) { prevRank = rank; } } + if (failureExists) { + var sortedMemberIndexes = members.map(function (_, i) { return i; }).sort(function (ai, bi) { + var a = members[ai]; + var b = members[bi]; + // first, sort by member rank + var rankDiff = _this.memberRank(a) - _this.memberRank(b); + if (rankDiff !== 0) { + return rankDiff; + } + // then lexicographically if alphabetize == true + if (_this.options.alphabetize && a.name !== undefined && b.name !== undefined) { + var aName = nameString(a.name); + var bName = nameString(b.name); + var nameDiff = aName.localeCompare(bName); + if (nameDiff !== 0) { + return nameDiff; + } + } + // finally, sort by position in original NodeArray so the sort remains stable. + return ai - bi; + }); + var splits_1 = getSplitIndexes(members, this.sourceFile.text); + var sortedMembersText = sortedMemberIndexes.map(function (i) { + var start = splits_1[i]; + var end = splits_1[i + 1]; + var nodeText = _this.sourceFile.text.substring(start, end); + while (true) { + // check if there are previous fixes which we need to merge into this one + // if yes, remove it from the list so that we do not return overlapping Replacements + var fixIndex = arrayFindLastIndex(_this.fixes, function (_a) { + var r = _a[1]; + return r.start >= start && r.start + r.length <= end; + }); + if (fixIndex === -1) { + break; + } + var fix = _this.fixes.splice(fixIndex, 1)[0]; + var replacement = fix[1]; + nodeText = applyReplacementOffset(nodeText, replacement, start); + } + return nodeText; + }); + // instead of assigning the fix immediately to the last failure, we temporarily store it in `this.fixes`, + // in case a containing node needs to be fixed too. We only "add" the fix to the last failure, although + // it fixes all failures in this NodeArray, as TSLint doesn't handle duplicate Replacements. + this.fixes.push([ + arrayLast(this.failures), + Lint.Replacement.replaceFromTo(splits_1[0], arrayLast(splits_1), sortedMembersText.join("")), + ]); + } }; /** Finds the lowest name higher than 'targetName'. */ MemberOrderingWalker.prototype.findLowerName = function (members, targetRank, targetName) { @@ -303,8 +376,8 @@ function memberKindFromName(name) { } } function getMemberKind(member) { - var accessLevel = hasModifier(ts.SyntaxKind.PrivateKeyword) ? "private" - : hasModifier(ts.SyntaxKind.ProtectedKeyword) ? "protected" + var accessLevel = tsutils_1.hasModifier(member.modifiers, ts.SyntaxKind.PrivateKeyword) ? "private" + : tsutils_1.hasModifier(member.modifiers, ts.SyntaxKind.ProtectedKeyword) ? "protected" : "public"; switch (member.kind) { case ts.SyntaxKind.Constructor: @@ -320,12 +393,9 @@ function getMemberKind(member) { return undefined; } function methodOrField(isMethod) { - var membership = hasModifier(ts.SyntaxKind.StaticKeyword) ? "Static" : "Instance"; + var membership = tsutils_1.hasModifier(member.modifiers, ts.SyntaxKind.StaticKeyword) ? "Static" : "Instance"; return memberKindForMethodOrField(accessLevel, membership, isMethod ? "Method" : "Field"); } - function hasModifier(kind) { - return Lint.hasModifier(member.modifiers, kind); - } } var MemberCategory = /** @class */ (function () { function MemberCategory(name, kinds) { @@ -436,4 +506,110 @@ function nameString(name) { return ""; } } -var _a; +/** + * Returns the last element of an array. (Or undefined). + */ +function arrayLast(array) { + return array[array.length - 1]; +} +/** + * Array.prototype.findIndex, but the last index. + */ +function arrayFindLastIndex(array, predicate) { + for (var i = array.length; i-- > 0;) { + if (predicate(array[i], i, array)) { + return i; + } + } + return -1; +} +/** + * Applies a Replacement to a part of the text which starts at offset. + * See also Replacement.apply + */ +function applyReplacementOffset(content, replacement, offset) { + return content.substring(0, replacement.start - offset) + + replacement.text + + content.substring(replacement.start - offset + replacement.length); +} +/** + * Get the indexes of the boundaries between nodes in the node array. The following points must be taken into account: + * - Trivia should stay with its corresponding node (comments on the same line following the token belong to the + * previous token, the rest to the next). + * - Reordering the subtexts should not result in code being commented out due to being moved between a "//" and + * the following newline. + * - The end of one node must be the start of the next, otherwise the intravening whitespace will be lost when + * reordering. + * + * Hence, the boundaries are chosen to be _after_ the newline following the node, or the beginning of the next token, + * if that comes first. + */ +function getSplitIndexes(members, text) { + var result = members.map(function (member) { return getNextSplitIndex(text, member.getFullStart()); }); + result.push(getNextSplitIndex(text, arrayLast(members).getEnd())); + return result; +} +/** + * Calculates the index after the newline following pos, or the beginning of the next token, whichever comes first. + * See also getSplitIndexes. + * This method is a modified version of TypeScript's internal iterateCommentRanges function. + */ +function getNextSplitIndex(text, pos) { + scan: while (pos >= 0 && pos < text.length) { + var ch = text.charCodeAt(pos); + switch (ch) { + case 13 /* carriageReturn */: + if (text.charCodeAt(pos + 1) === 10 /* lineFeed */) { + pos++; + } + // falls through + case 10 /* lineFeed */: + pos++; + // split is after new line + return pos; + case 9 /* tab */: + case 11 /* verticalTab */: + case 12 /* formFeed */: + case 32 /* space */: + // skip whitespace + pos++; + continue; + case 47 /* slash */: + var nextChar = text.charCodeAt(pos + 1); + if (nextChar === 47 /* slash */ || nextChar === 42 /* asterisk */) { + var isSingleLineComment = nextChar === 47 /* slash */; + pos += 2; + if (isSingleLineComment) { + while (pos < text.length) { + if (ts.isLineBreak(text.charCodeAt(pos))) { + // the comment ends here, go back to default logic to handle parsing new line and result + continue scan; + } + pos++; + } + } + else { + while (pos < text.length) { + if (text.charCodeAt(pos) === 42 /* asterisk */ && text.charCodeAt(pos + 1) === 47 /* slash */) { + pos += 2; + continue scan; + } + pos++; + } + } + // if we arrive here, it's because pos == text.length + return pos; + } + break scan; + default: + // skip whitespace: + if (ch > 127 /* maxAsciiCharacter */ && (ts.isWhiteSpaceLike(ch))) { + pos++; + continue; + } + break scan; + } + } + return pos; +} +var templateObject_1, templateObject_2; |