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 +1 -0
- package/dist/index.d.mts +26 -4
- package/dist/index.mjs +228 -7
- package/package.json +4 -2
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.
|
|
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$
|
|
1194
|
-
const messages$
|
|
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$
|
|
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$
|
|
1545
|
-
create: create$
|
|
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$
|
|
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.
|
|
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",
|