memberOrderingRule.js 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. "use strict";
  2. /**
  3. * @license
  4. * Copyright 2013 Palantir Technologies, Inc.
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. Object.defineProperty(exports, "__esModule", { value: true });
  19. var tslib_1 = require("tslib");
  20. var tsutils_1 = require("tsutils");
  21. var ts = require("typescript");
  22. var error_1 = require("../error");
  23. var Lint = require("../index");
  24. var utils_1 = require("../utils");
  25. var OPTION_ORDER = "order";
  26. var OPTION_ALPHABETIZE = "alphabetize";
  27. var MemberKind;
  28. (function (MemberKind) {
  29. MemberKind[MemberKind["publicStaticField"] = 0] = "publicStaticField";
  30. MemberKind[MemberKind["publicStaticMethod"] = 1] = "publicStaticMethod";
  31. MemberKind[MemberKind["protectedStaticField"] = 2] = "protectedStaticField";
  32. MemberKind[MemberKind["protectedStaticMethod"] = 3] = "protectedStaticMethod";
  33. MemberKind[MemberKind["privateStaticField"] = 4] = "privateStaticField";
  34. MemberKind[MemberKind["privateStaticMethod"] = 5] = "privateStaticMethod";
  35. MemberKind[MemberKind["publicInstanceField"] = 6] = "publicInstanceField";
  36. MemberKind[MemberKind["protectedInstanceField"] = 7] = "protectedInstanceField";
  37. MemberKind[MemberKind["privateInstanceField"] = 8] = "privateInstanceField";
  38. MemberKind[MemberKind["publicConstructor"] = 9] = "publicConstructor";
  39. MemberKind[MemberKind["protectedConstructor"] = 10] = "protectedConstructor";
  40. MemberKind[MemberKind["privateConstructor"] = 11] = "privateConstructor";
  41. MemberKind[MemberKind["publicInstanceMethod"] = 12] = "publicInstanceMethod";
  42. MemberKind[MemberKind["protectedInstanceMethod"] = 13] = "protectedInstanceMethod";
  43. MemberKind[MemberKind["privateInstanceMethod"] = 14] = "privateInstanceMethod";
  44. })(MemberKind || (MemberKind = {}));
  45. var PRESETS = new Map([
  46. ["fields-first", [
  47. "public-static-field",
  48. "protected-static-field",
  49. "private-static-field",
  50. "public-instance-field",
  51. "protected-instance-field",
  52. "private-instance-field",
  53. "constructor",
  54. "public-static-method",
  55. "protected-static-method",
  56. "private-static-method",
  57. "public-instance-method",
  58. "protected-instance-method",
  59. "private-instance-method",
  60. ]],
  61. ["instance-sandwich", [
  62. "public-static-field",
  63. "protected-static-field",
  64. "private-static-field",
  65. "public-instance-field",
  66. "protected-instance-field",
  67. "private-instance-field",
  68. "constructor",
  69. "public-instance-method",
  70. "protected-instance-method",
  71. "private-instance-method",
  72. "public-static-method",
  73. "protected-static-method",
  74. "private-static-method",
  75. ]],
  76. ["statics-first", [
  77. "public-static-field",
  78. "public-static-method",
  79. "protected-static-field",
  80. "protected-static-method",
  81. "private-static-field",
  82. "private-static-method",
  83. "public-instance-field",
  84. "protected-instance-field",
  85. "private-instance-field",
  86. "constructor",
  87. "public-instance-method",
  88. "protected-instance-method",
  89. "private-instance-method",
  90. ]],
  91. ]);
  92. var PRESET_NAMES = Array.from(PRESETS.keys());
  93. var allMemberKindNames = utils_1.mapDefined(Object.keys(MemberKind), function (key) {
  94. var mk = MemberKind[key];
  95. return typeof mk === "number" ? MemberKind[mk].replace(/[A-Z]/g, function (cap) { return "-" + cap.toLowerCase(); }) : undefined;
  96. });
  97. function namesMarkdown(names) {
  98. return names.map(function (name) { return "* `" + name + "`"; }).join("\n ");
  99. }
  100. 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);
  101. var Rule = /** @class */ (function (_super) {
  102. tslib_1.__extends(Rule, _super);
  103. function Rule() {
  104. return _super !== null && _super.apply(this, arguments) || this;
  105. }
  106. Rule.FAILURE_STRING_ALPHABETIZE = function (prevName, curName) {
  107. return show(curName) + " should come alphabetically before " + show(prevName);
  108. function show(s) {
  109. return s === "" ? "Computed property" : "'" + s + "'";
  110. }
  111. };
  112. /* tslint:enable:object-literal-sort-keys */
  113. Rule.prototype.apply = function (sourceFile) {
  114. var options;
  115. try {
  116. options = parseOptions(this.ruleArguments);
  117. }
  118. catch (e) {
  119. error_1.showWarningOnce("Warning: " + this.ruleName + " - " + e.message);
  120. return [];
  121. }
  122. return this.applyWithWalker(new MemberOrderingWalker(sourceFile, this.ruleName, options));
  123. };
  124. /* tslint:disable:object-literal-sort-keys */
  125. Rule.metadata = {
  126. ruleName: "member-ordering",
  127. description: "Enforces member ordering.",
  128. 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 clases 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 clases with multiple separate groups, consider splitting class responsibilities\n apart across multiple single-responsibility classes.\n "]))),
  129. optionsDescription: optionsDescription,
  130. options: {
  131. type: "object",
  132. properties: {
  133. order: {
  134. oneOf: [
  135. {
  136. type: "string",
  137. enum: PRESET_NAMES,
  138. },
  139. {
  140. type: "array",
  141. items: {
  142. type: "string",
  143. enum: allMemberKindNames,
  144. },
  145. maxLength: 13,
  146. },
  147. ],
  148. },
  149. },
  150. additionalProperties: false,
  151. },
  152. optionExamples: [
  153. [true, { order: "fields-first" }],
  154. [true, {
  155. order: [
  156. "public-static-field",
  157. "public-instance-field",
  158. "public-constructor",
  159. "private-static-field",
  160. "private-instance-field",
  161. "private-constructor",
  162. "public-instance-method",
  163. "protected-instance-method",
  164. "private-instance-method",
  165. ],
  166. }],
  167. [true, {
  168. order: [
  169. {
  170. name: "static non-private",
  171. kinds: [
  172. "public-static-field",
  173. "protected-static-field",
  174. "public-static-method",
  175. "protected-static-method",
  176. ],
  177. },
  178. "constructor",
  179. ],
  180. }],
  181. ],
  182. type: "typescript",
  183. typescriptOnly: false,
  184. };
  185. return Rule;
  186. }(Lint.Rules.AbstractRule));
  187. exports.Rule = Rule;
  188. var MemberOrderingWalker = /** @class */ (function (_super) {
  189. tslib_1.__extends(MemberOrderingWalker, _super);
  190. function MemberOrderingWalker() {
  191. return _super !== null && _super.apply(this, arguments) || this;
  192. }
  193. MemberOrderingWalker.prototype.walk = function (sourceFile) {
  194. var _this = this;
  195. var cb = function (node) {
  196. switch (node.kind) {
  197. case ts.SyntaxKind.ClassDeclaration:
  198. case ts.SyntaxKind.ClassExpression:
  199. case ts.SyntaxKind.InterfaceDeclaration:
  200. case ts.SyntaxKind.TypeLiteral:
  201. _this.checkMembers(node.members);
  202. }
  203. return ts.forEachChild(node, cb);
  204. };
  205. return ts.forEachChild(sourceFile, cb);
  206. };
  207. MemberOrderingWalker.prototype.checkMembers = function (members) {
  208. var prevRank = -1;
  209. var prevName;
  210. for (var _i = 0, members_1 = members; _i < members_1.length; _i++) {
  211. var member = members_1[_i];
  212. var rank = this.memberRank(member);
  213. if (rank === -1) {
  214. // no explicit ordering for this kind of node specified, so continue
  215. continue;
  216. }
  217. if (rank < prevRank) {
  218. var nodeType = this.rankName(rank);
  219. var prevNodeType = this.rankName(prevRank);
  220. var lowerRank = this.findLowerRank(members, rank);
  221. var locationHint = lowerRank !== -1
  222. ? "after " + this.rankName(lowerRank) + "s"
  223. : "at the beginning of the class/interface";
  224. var errorLine1 = "Declaration of " + nodeType + " not allowed after declaration of " + prevNodeType + ". " +
  225. ("Instead, this should come " + locationHint + ".");
  226. this.addFailureAtNode(member, errorLine1);
  227. }
  228. else {
  229. if (this.options.alphabetize && member.name !== undefined) {
  230. if (rank !== prevRank) {
  231. // No alphabetical ordering between different ranks
  232. prevName = undefined;
  233. }
  234. var curName = nameString(member.name);
  235. if (prevName !== undefined && caseInsensitiveLess(curName, prevName)) {
  236. this.addFailureAtNode(member.name, Rule.FAILURE_STRING_ALPHABETIZE(this.findLowerName(members, rank, curName), curName));
  237. }
  238. else {
  239. prevName = curName;
  240. }
  241. }
  242. // keep track of last good node
  243. prevRank = rank;
  244. }
  245. }
  246. };
  247. /** Finds the lowest name higher than 'targetName'. */
  248. MemberOrderingWalker.prototype.findLowerName = function (members, targetRank, targetName) {
  249. for (var _i = 0, members_2 = members; _i < members_2.length; _i++) {
  250. var member = members_2[_i];
  251. if (member.name === undefined || this.memberRank(member) !== targetRank) {
  252. continue;
  253. }
  254. var name = nameString(member.name);
  255. if (caseInsensitiveLess(targetName, name)) {
  256. return name;
  257. }
  258. }
  259. throw new Error("Expected to find a name");
  260. };
  261. /** Finds the highest existing rank lower than `targetRank`. */
  262. MemberOrderingWalker.prototype.findLowerRank = function (members, targetRank) {
  263. var max = -1;
  264. for (var _i = 0, members_3 = members; _i < members_3.length; _i++) {
  265. var member = members_3[_i];
  266. var rank = this.memberRank(member);
  267. if (rank !== -1 && rank < targetRank) {
  268. max = Math.max(max, rank);
  269. }
  270. }
  271. return max;
  272. };
  273. MemberOrderingWalker.prototype.memberRank = function (member) {
  274. var optionName = getMemberKind(member);
  275. if (optionName === undefined) {
  276. return -1;
  277. }
  278. return this.options.order.findIndex(function (category) { return category.has(optionName); });
  279. };
  280. MemberOrderingWalker.prototype.rankName = function (rank) {
  281. return this.options.order[rank].name;
  282. };
  283. return MemberOrderingWalker;
  284. }(Lint.AbstractWalker));
  285. function caseInsensitiveLess(a, b) {
  286. return a.toLowerCase() < b.toLowerCase();
  287. }
  288. function memberKindForConstructor(access) {
  289. return MemberKind[access + "Constructor"];
  290. }
  291. function memberKindForMethodOrField(access, membership, kind) {
  292. return MemberKind[access + membership + kind];
  293. }
  294. var allAccess = ["public", "protected", "private"];
  295. function memberKindFromName(name) {
  296. var kind = MemberKind[Lint.Utils.camelize(name)];
  297. return typeof kind === "number" ? [kind] : allAccess.map(addModifier);
  298. function addModifier(modifier) {
  299. var modifiedKind = MemberKind[Lint.Utils.camelize(modifier + "-" + name)];
  300. if (typeof modifiedKind !== "number") {
  301. throw new Error("Bad member kind: " + name);
  302. }
  303. return modifiedKind;
  304. }
  305. }
  306. function getMemberKind(member) {
  307. var accessLevel = tsutils_1.hasModifier(member.modifiers, ts.SyntaxKind.PrivateKeyword) ? "private"
  308. : tsutils_1.hasModifier(member.modifiers, ts.SyntaxKind.ProtectedKeyword) ? "protected"
  309. : "public";
  310. switch (member.kind) {
  311. case ts.SyntaxKind.Constructor:
  312. case ts.SyntaxKind.ConstructSignature:
  313. return memberKindForConstructor(accessLevel);
  314. case ts.SyntaxKind.PropertyDeclaration:
  315. case ts.SyntaxKind.PropertySignature:
  316. return methodOrField(isFunctionLiteral(member.initializer));
  317. case ts.SyntaxKind.MethodDeclaration:
  318. case ts.SyntaxKind.MethodSignature:
  319. return methodOrField(true);
  320. default:
  321. return undefined;
  322. }
  323. function methodOrField(isMethod) {
  324. var membership = tsutils_1.hasModifier(member.modifiers, ts.SyntaxKind.StaticKeyword) ? "Static" : "Instance";
  325. return memberKindForMethodOrField(accessLevel, membership, isMethod ? "Method" : "Field");
  326. }
  327. }
  328. var MemberCategory = /** @class */ (function () {
  329. function MemberCategory(name, kinds) {
  330. this.name = name;
  331. this.kinds = kinds;
  332. }
  333. MemberCategory.prototype.has = function (kind) { return this.kinds.has(kind); };
  334. return MemberCategory;
  335. }());
  336. function parseOptions(options) {
  337. var _a = getOptionsJson(options), orderJson = _a.order, alphabetize = _a.alphabetize;
  338. var order = orderJson.map(function (cat) { return typeof cat === "string"
  339. ? new MemberCategory(cat.replace(/-/g, " "), new Set(memberKindFromName(cat)))
  340. : new MemberCategory(cat.name, new Set(utils_1.flatMap(cat.kinds, memberKindFromName))); });
  341. return { order: order, alphabetize: alphabetize };
  342. }
  343. function getOptionsJson(allOptions) {
  344. if (allOptions == undefined || allOptions.length === 0 || allOptions[0] == undefined) {
  345. throw new Error("Got empty options");
  346. }
  347. var firstOption = allOptions[0];
  348. if (typeof firstOption !== "object") {
  349. // Undocumented direct string option. Deprecate eventually.
  350. return { order: convertFromOldStyleOptions(allOptions), alphabetize: false }; // presume allOptions to be string[]
  351. }
  352. return { order: categoryFromOption(firstOption[OPTION_ORDER]), alphabetize: firstOption[OPTION_ALPHABETIZE] === true };
  353. }
  354. function categoryFromOption(orderOption) {
  355. if (Array.isArray(orderOption)) {
  356. return orderOption;
  357. }
  358. var preset = PRESETS.get(orderOption);
  359. if (preset === undefined) {
  360. throw new Error("Bad order: " + JSON.stringify(orderOption));
  361. }
  362. return preset;
  363. }
  364. /**
  365. * Convert from undocumented old-style options.
  366. * This is designed to mimic the old behavior and should be removed eventually.
  367. */
  368. function convertFromOldStyleOptions(options) {
  369. var categories = [{ name: "member", kinds: allMemberKindNames }];
  370. if (hasOption("variables-before-functions")) {
  371. categories = splitOldStyleOptions(categories, function (kind) { return kind.includes("field"); }, "field", "method");
  372. }
  373. if (hasOption("static-before-instance")) {
  374. categories = splitOldStyleOptions(categories, function (kind) { return kind.includes("static"); }, "static", "instance");
  375. }
  376. if (hasOption("public-before-private")) {
  377. // 'protected' is considered public
  378. categories = splitOldStyleOptions(categories, function (kind) { return !kind.includes("private"); }, "public", "private");
  379. }
  380. return categories;
  381. function hasOption(x) {
  382. return options.indexOf(x) !== -1;
  383. }
  384. }
  385. function splitOldStyleOptions(categories, filter, a, b) {
  386. var newCategories = [];
  387. var _loop_1 = function (cat) {
  388. var yes = [];
  389. var no = [];
  390. for (var _i = 0, _a = cat.kinds; _i < _a.length; _i++) {
  391. var kind = _a[_i];
  392. if (filter(kind)) {
  393. yes.push(kind);
  394. }
  395. else {
  396. no.push(kind);
  397. }
  398. }
  399. var augmentName = function (s) {
  400. if (a === "field") {
  401. // Replace "member" with "field"/"method" instead of augmenting.
  402. return s;
  403. }
  404. return s + " " + cat.name;
  405. };
  406. newCategories.push({ name: augmentName(a), kinds: yes });
  407. newCategories.push({ name: augmentName(b), kinds: no });
  408. };
  409. for (var _i = 0, categories_1 = categories; _i < categories_1.length; _i++) {
  410. var cat = categories_1[_i];
  411. _loop_1(cat);
  412. }
  413. return newCategories;
  414. }
  415. function isFunctionLiteral(node) {
  416. if (node === undefined) {
  417. return false;
  418. }
  419. switch (node.kind) {
  420. case ts.SyntaxKind.ArrowFunction:
  421. case ts.SyntaxKind.FunctionExpression:
  422. return true;
  423. default:
  424. return false;
  425. }
  426. }
  427. function nameString(name) {
  428. switch (name.kind) {
  429. case ts.SyntaxKind.Identifier:
  430. case ts.SyntaxKind.StringLiteral:
  431. case ts.SyntaxKind.NumericLiteral:
  432. return name.text;
  433. default:
  434. return "";
  435. }
  436. }
  437. var templateObject_1, templateObject_2;