eslint-plugin-flawless 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -179,6 +179,7 @@ Requires [type information](https://typescript-eslint.io/linting/typed-linting).
179
179
  | Name                       | Description | 🔧 | 💭 |
180
180
  | :---------------------------------------------------------------------------------- | :-------------------------------------------------------------- | :-- | :-- |
181
181
  | [naming-convention](src/rules/naming-convention/documentation.md) | Enforce naming conventions for everything across a codebase | | 💭 |
182
+ | [toml-sort-keys](src/rules/toml-sort-keys/documentation.md) | Enforce a configured sort order for TOML keys and tables | 🔧 | |
182
183
  | [yaml-block-key-blank-lines](src/rules/yaml-block-key-blank-lines/documentation.md) | Enforce blank lines around top-level YAML block collection keys | 🔧 | |
183
184
 
184
185
  <!-- end auto-generated rules list -->
package/dist/index.d.mts CHANGED
@@ -116,7 +116,20 @@ interface NamingSelector {
116
116
  //#endregion
117
117
  //#region src/rules/naming-convention/rule.d.ts
118
118
  type MessageIds = "doesNotMatchFormat" | "doesNotMatchFormatTrimmed" | "missingAffix" | "missingUnderscore" | "satisfyCustom" | "unexpectedUnderscore";
119
- type Options = Array<NamingSelector>;
119
+ type Options$1 = Array<NamingSelector>;
120
+ //#endregion
121
+ //#region src/rules/toml-sort-keys/rule.d.ts
122
+ type Options = Array<SortSpec>;
123
+ interface SortOrderObject {
124
+ caseSensitive?: boolean;
125
+ natural?: boolean;
126
+ type?: "asc" | "desc";
127
+ }
128
+ type SortOrder = Array<string> | SortOrderObject;
129
+ interface SortSpec {
130
+ order: SortOrder;
131
+ pathPattern: string;
132
+ }
120
133
  //#endregion
121
134
  //#region src/plugin.d.ts
122
135
  declare const plugin: {
@@ -125,7 +138,10 @@ declare const plugin: {
125
138
  version: string;
126
139
  };
127
140
  rules: {
128
- "naming-convention": TSESLint.RuleModule<MessageIds, Options, PluginDocumentation, TSESLint.RuleListener> & {
141
+ "naming-convention": TSESLint.RuleModule<MessageIds, Options$1, PluginDocumentation, TSESLint.RuleListener> & {
142
+ name: string;
143
+ };
144
+ "toml-sort-keys": TSESLint.RuleModule<"unsorted", Options, PluginDocumentation, TSESLint.RuleListener> & {
129
145
  name: string;
130
146
  };
131
147
  "yaml-block-key-blank-lines": TSESLint.RuleModule<"blankLine", [], PluginDocumentation, TSESLint.RuleListener> & {
@@ -145,7 +161,10 @@ declare const _default: {
145
161
  version: string;
146
162
  };
147
163
  rules: {
148
- "naming-convention": import("${configDir}").RuleModule<MessageIds, Options, PluginDocumentation, import("${configDir}").RuleListener> & {
164
+ "naming-convention": import("${configDir}").RuleModule<MessageIds, Options$1, PluginDocumentation, import("${configDir}").RuleListener> & {
165
+ name: string;
166
+ };
167
+ "toml-sort-keys": import("${configDir}").RuleModule<"unsorted", Options, PluginDocumentation, import("${configDir}").RuleListener> & {
149
168
  name: string;
150
169
  };
151
170
  "yaml-block-key-blank-lines": import("${configDir}").RuleModule<"blankLine", [], PluginDocumentation, import("${configDir}").RuleListener> & {
@@ -162,7 +181,10 @@ declare const _default: {
162
181
  version: string;
163
182
  };
164
183
  rules: {
165
- "naming-convention": import("${configDir}").RuleModule<MessageIds, Options, PluginDocumentation, import("${configDir}").RuleListener> & {
184
+ "naming-convention": import("${configDir}").RuleModule<MessageIds, Options$1, PluginDocumentation, import("${configDir}").RuleListener> & {
185
+ name: string;
186
+ };
187
+ "toml-sort-keys": import("${configDir}").RuleModule<"unsorted", Options, PluginDocumentation, import("${configDir}").RuleListener> & {
166
188
  name: string;
167
189
  };
168
190
  "yaml-block-key-blank-lines": import("${configDir}").RuleModule<"blankLine", [], PluginDocumentation, import("${configDir}").RuleListener> & {
package/dist/index.mjs CHANGED
@@ -3,9 +3,10 @@ import { requiresQuoting } from "@typescript-eslint/type-utils";
3
3
  import { ASTUtils, AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
4
4
  import { RuleCreator, getParserServices } from "@typescript-eslint/utils/eslint-utils";
5
5
  import assert from "node:assert";
6
+ import { getStaticTOMLValue } from "toml-eslint-parser";
6
7
  //#region package.json
7
8
  var name = "eslint-plugin-flawless";
8
- var version = "0.1.1";
9
+ var version = "0.1.2";
9
10
  var repository = {
10
11
  "url": "git+https://github.com/christopher-buss/eslint-plugin-flawless.git",
11
12
  "type": "git"
@@ -1190,8 +1191,8 @@ const SCHEMA = {
1190
1191
  };
1191
1192
  //#endregion
1192
1193
  //#region src/rules/naming-convention/rule.ts
1193
- const RULE_NAME$1 = "naming-convention";
1194
- const messages$1 = {
1194
+ const RULE_NAME$2 = "naming-convention";
1195
+ const messages$2 = {
1195
1196
  doesNotMatchFormat: "{{type}} name `{{name}}` must match one of the following formats: {{formats}}",
1196
1197
  doesNotMatchFormatTrimmed: "{{type}} name `{{name}}` trimmed as `{{processedName}}` must match one of the following formats: {{formats}}",
1197
1198
  missingAffix: "{{type}} name `{{name}}` must have one of the following {{position}}es: {{affixes}}",
@@ -1221,7 +1222,7 @@ const camelCaseNamingConfig = [
1221
1222
  selector: "typeLike"
1222
1223
  }
1223
1224
  ];
1224
- function create$1(contextWithoutDefaults) {
1225
+ function create$2(contextWithoutDefaults) {
1225
1226
  const context = contextWithoutDefaults.options.length > 0 ? contextWithoutDefaults : Object.setPrototypeOf({ options: camelCaseNamingConfig }, contextWithoutDefaults);
1226
1227
  const validators = parseOptions(context);
1227
1228
  const compilerOptions = getParserServices(context, true).program?.getCompilerOptions() ?? {};
@@ -1541,8 +1542,8 @@ function create$1(contextWithoutDefaults) {
1541
1542
  }));
1542
1543
  }
1543
1544
  const namingConvention = createEslintRule({
1544
- name: RULE_NAME$1,
1545
- create: create$1,
1545
+ name: RULE_NAME$2,
1546
+ create: create$2,
1546
1547
  defaultOptions: camelCaseNamingConfig,
1547
1548
  meta: {
1548
1549
  docs: {
@@ -1552,7 +1553,7 @@ const namingConvention = createEslintRule({
1552
1553
  },
1553
1554
  fixable: void 0,
1554
1555
  hasSuggestions: false,
1555
- messages: messages$1,
1556
+ messages: messages$2,
1556
1557
  schema: SCHEMA,
1557
1558
  type: "suggestion"
1558
1559
  }
@@ -1582,6 +1583,225 @@ function requiresQuoting$1(node, target) {
1582
1583
  return requiresQuoting(node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.PrivateIdentifier ? node.name : `${node.value}`, target);
1583
1584
  }
1584
1585
  //#endregion
1586
+ //#region src/rules/toml-sort-keys/rule.ts
1587
+ const RULE_NAME$1 = "toml-sort-keys";
1588
+ const MESSAGE_ID = "unsorted";
1589
+ const messages$1 = { [MESSAGE_ID]: "Expected {{target}} to follow the configured order." };
1590
+ const schema = {
1591
+ items: {
1592
+ additionalProperties: false,
1593
+ properties: {
1594
+ order: { oneOf: [{
1595
+ items: { type: "string" },
1596
+ type: "array"
1597
+ }, {
1598
+ additionalProperties: false,
1599
+ properties: {
1600
+ caseSensitive: { type: "boolean" },
1601
+ natural: { type: "boolean" },
1602
+ type: {
1603
+ enum: ["asc", "desc"],
1604
+ type: "string"
1605
+ }
1606
+ },
1607
+ type: "object"
1608
+ }] },
1609
+ pathPattern: { type: "string" }
1610
+ },
1611
+ required: ["order", "pathPattern"],
1612
+ type: "object"
1613
+ },
1614
+ type: "array"
1615
+ };
1616
+ function isTable(entry) {
1617
+ return entry.type === "TOMLTable";
1618
+ }
1619
+ /**
1620
+ * The dotted name of an entry, e.g. `settings`, `settings.node`, `_.path`.
1621
+ *
1622
+ * @param entry - The table header or key-value entry.
1623
+ * @returns The dotted key path as a string.
1624
+ */
1625
+ function nameOf(entry) {
1626
+ return getStaticTOMLValue(entry.key).join(".");
1627
+ }
1628
+ /**
1629
+ * Builds a rank function from an explicit order list. An entry matches an order
1630
+ * item when it equals it or is a dotted child of it (`settings.node` matches
1631
+ * `settings`), which keeps sub-tables grouped under their parent's slot.
1632
+ *
1633
+ * @param order - The configured names in their desired order.
1634
+ * @returns A function giving a name's rank, or `Infinity` when unlisted.
1635
+ */
1636
+ function makeRank(order) {
1637
+ return (name) => {
1638
+ for (const [index, item] of order.entries()) if (name === item || name.startsWith(`${item}.`)) return index;
1639
+ return Number.POSITIVE_INFINITY;
1640
+ };
1641
+ }
1642
+ function makeFallback(config) {
1643
+ const { caseSensitive = true, natural = false, type = "asc" } = config;
1644
+ const sensitivity = caseSensitive ? "variant" : "accent";
1645
+ return (a, b) => {
1646
+ const result = a.localeCompare(b, "en", {
1647
+ numeric: natural,
1648
+ sensitivity
1649
+ });
1650
+ return type === "desc" ? -result : result;
1651
+ };
1652
+ }
1653
+ /**
1654
+ * Builds a comparator for one table body. Explicit-order entries sort first by
1655
+ * their listed position, then unlisted entries fall back to a natural/asc sort
1656
+ * (mirroring `yaml/sort-keys`). At the top level, bare key-values are always
1657
+ * kept before any `[table]` header, since TOML would otherwise re-scope them.
1658
+ *
1659
+ * @param order - The order configuration for this table's path.
1660
+ * @param isTopLevel - Whether the body is the top-level table.
1661
+ * @returns A comparator over two entries.
1662
+ */
1663
+ function makeComparator(order, isTopLevel) {
1664
+ const rank = Array.isArray(order) ? makeRank(order) : void 0;
1665
+ const fallback = makeFallback(Array.isArray(order) ? {
1666
+ natural: true,
1667
+ type: "asc"
1668
+ } : order);
1669
+ return (a, b) => {
1670
+ if (isTopLevel) {
1671
+ const aRank = isTable(a) ? 1 : 0;
1672
+ const bRank = isTable(b) ? 1 : 0;
1673
+ if (aRank !== bRank) return aRank - bRank;
1674
+ }
1675
+ if (rank !== void 0) {
1676
+ const aRank = rank(nameOf(a));
1677
+ const bRank = rank(nameOf(b));
1678
+ if (aRank !== bRank) {
1679
+ if (aRank === Number.POSITIVE_INFINITY) return 1;
1680
+ if (bRank === Number.POSITIVE_INFINITY) return -1;
1681
+ return aRank - bRank;
1682
+ }
1683
+ }
1684
+ return fallback(nameOf(a), nameOf(b));
1685
+ };
1686
+ }
1687
+ function create$1(context) {
1688
+ const { options, sourceCode } = context;
1689
+ if (sourceCode.parserServices.isTOML !== true) return {};
1690
+ const specs = options.map(({ order, pathPattern }) => {
1691
+ return {
1692
+ order,
1693
+ pattern: new RegExp(pathPattern, "u")
1694
+ };
1695
+ });
1696
+ if (specs.length === 0) return {};
1697
+ function resolveOrder(path) {
1698
+ for (const spec of specs) if (spec.pattern.test(path)) return spec.order;
1699
+ }
1700
+ /**
1701
+ * A comment counts as attached to an entry when it sits on its own line
1702
+ * directly above it (no blank line, no trailing code). Such comments travel
1703
+ * with the entry when it moves.
1704
+ *
1705
+ * @param comment - The comment to classify.
1706
+ * @returns Whether the comment stands alone on its line.
1707
+ */
1708
+ function isOwnLineComment(comment) {
1709
+ const before = sourceCode.getTokenBefore(comment, { includeComments: true });
1710
+ return before === null || before.loc.end.line < comment.loc.start.line;
1711
+ }
1712
+ function leadingStart(entry) {
1713
+ const comments = sourceCode.getCommentsBefore(entry);
1714
+ let start = entry.range[0];
1715
+ let boundaryLine = entry.loc.start.line;
1716
+ for (let index = comments.length - 1; index >= 0; index -= 1) {
1717
+ const comment = comments[index];
1718
+ if (comment === void 0) break;
1719
+ if (comment.loc.end.line !== boundaryLine - 1 || !isOwnLineComment(comment)) break;
1720
+ start = comment.range[0];
1721
+ boundaryLine = comment.loc.start.line;
1722
+ }
1723
+ return start;
1724
+ }
1725
+ function trailingEnd(entry) {
1726
+ const [comment] = sourceCode.getCommentsAfter(entry);
1727
+ if (comment?.loc.start.line === entry.loc.end.line) return comment.range[1];
1728
+ return entry.range[1];
1729
+ }
1730
+ function buildFix(entries, target) {
1731
+ const text = sourceCode.getText();
1732
+ const blocks = entries.map((entry) => {
1733
+ return {
1734
+ end: trailingEnd(entry),
1735
+ entry,
1736
+ start: leadingStart(entry)
1737
+ };
1738
+ });
1739
+ for (let index = 1; index < blocks.length; index += 1) {
1740
+ const previous = blocks[index - 1];
1741
+ const current = blocks[index];
1742
+ if (previous === void 0 || current === void 0) return;
1743
+ if (/\S/u.test(text.slice(previous.end, current.start))) return;
1744
+ }
1745
+ const first = blocks[0];
1746
+ const last = blocks[blocks.length - 1];
1747
+ if (first === void 0 || last === void 0) return;
1748
+ const textByEntry = new Map(blocks.map((block) => [block.entry, text.slice(block.start, block.end)]));
1749
+ const parts = [];
1750
+ for (const [index, entry] of target.entries()) {
1751
+ const previous = target[index - 1];
1752
+ if (previous !== void 0) parts.push(isTable(entry) || isTable(previous) ? "\n\n" : "\n");
1753
+ parts.push(textByEntry.get(entry) ?? "");
1754
+ }
1755
+ const sortedText = parts.join("");
1756
+ return (fixer) => fixer.replaceTextRange([first.start, last.end], sortedText);
1757
+ }
1758
+ function verify(path, body, isTopLevel) {
1759
+ if (body.length < 2) return;
1760
+ const order = resolveOrder(path);
1761
+ if (order === void 0) return;
1762
+ const entries = [...body];
1763
+ const target = [...entries].sort(makeComparator(order, isTopLevel));
1764
+ let outOfPlace;
1765
+ for (const [index, entry] of entries.entries()) if (entry !== target[index]) {
1766
+ outOfPlace = entry;
1767
+ break;
1768
+ }
1769
+ if (outOfPlace === void 0) return;
1770
+ context.report({
1771
+ data: { target: path === "" ? "top-level tables" : `keys in "${path}"` },
1772
+ fix: buildFix(entries, target),
1773
+ loc: outOfPlace.key.loc,
1774
+ messageId: MESSAGE_ID
1775
+ });
1776
+ }
1777
+ return {
1778
+ TOMLTable(node) {
1779
+ verify(getStaticTOMLValue(node.key).join("."), node.body, false);
1780
+ },
1781
+ TOMLTopLevelTable(node) {
1782
+ verify("", node.body, true);
1783
+ }
1784
+ };
1785
+ }
1786
+ const tomlSortKeys = createEslintRule({
1787
+ name: RULE_NAME$1,
1788
+ create: create$1,
1789
+ defaultOptions: [],
1790
+ meta: {
1791
+ defaultOptions: [],
1792
+ docs: {
1793
+ description: "Enforce a configured sort order for TOML keys and tables",
1794
+ recommended: false,
1795
+ requiresTypeChecking: false
1796
+ },
1797
+ fixable: "code",
1798
+ hasSuggestions: false,
1799
+ messages: messages$1,
1800
+ schema,
1801
+ type: "layout"
1802
+ }
1803
+ });
1804
+ //#endregion
1585
1805
  //#region src/rules/yaml-block-key-blank-lines/rule.ts
1586
1806
  const RULE_NAME = "yaml-block-key-blank-lines";
1587
1807
  const messages = { blankLine: "Expected {{count}} blank line(s) around this top-level key." };
@@ -1659,6 +1879,7 @@ const plugin = {
1659
1879
  },
1660
1880
  rules: {
1661
1881
  "naming-convention": namingConvention,
1882
+ "toml-sort-keys": tomlSortKeys,
1662
1883
  "yaml-block-key-blank-lines": yamlBlockKeyBlankLines
1663
1884
  }
1664
1885
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-flawless",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Your ESLint plugin description",
5
5
  "keywords": [
6
6
  "eslint",
@@ -36,7 +36,8 @@
36
36
  "dependencies": {
37
37
  "@typescript-eslint/scope-manager": "8.62.1",
38
38
  "@typescript-eslint/type-utils": "8.62.1",
39
- "@typescript-eslint/utils": "8.62.1"
39
+ "@typescript-eslint/utils": "8.62.1",
40
+ "toml-eslint-parser": "1.0.3"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@antfu/ni": "30.2.0",
@@ -52,6 +53,7 @@
52
53
  "eslint-plugin-eslint-plugin": "7.4.1",
53
54
  "eslint-plugin-n": "18.2.1",
54
55
  "eslint-plugin-pnpm": "1.6.1",
56
+ "eslint-plugin-toml": "1.4.0",
55
57
  "eslint-plugin-yml": "3.5.0",
56
58
  "eslint-vitest-rule-tester": "3.1.0",
57
59
  "jiti": "2.7.0",