@starci/eslint-canon-be 1.0.0

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.
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The rules that hold `type-safety.md`.
3
+ *
4
+ * Three house rules plus two standard ones, and the split is worth knowing. `no-explicit-any` and
5
+ * the array-type spelling already exist in the TypeScript plugin and are named in `recommended`
6
+ * rather than reimplemented -- a second implementation of a rule everybody has is a maintenance
7
+ * cost with no gain.
8
+ *
9
+ * What is house-written is what the standard set does not cover: the double cast (TYPE-2), the
10
+ * inline parameter type (TYPE-3) and the const enum (TYPE-4). Each is a way of switching the
11
+ * compiler off that looks locally reasonable, which is exactly the class a rule is for.
12
+ *
13
+ * TYPE-5 is not enforced. Whether a set of booleans describes ONE situation or several genuinely
14
+ * independent ones needs to know what the code means, and a rule that guessed would fire on every
15
+ * struct with two flags in it.
16
+ */
17
+
18
+ /** Forward-slash form of a filename, so Windows paths compare like every other path. */
19
+ const normalizePath = (filename) => String(filename || "").replace(/\\/g, "/")
20
+
21
+ /** The spec family and the test tree may build a deliberately wrong value on purpose. */
22
+ const isTestFile = (filename) => {
23
+ const file = normalizePath(filename)
24
+ return /\.(?:spec|test|e2e-spec|int-spec|harness-spec)\.ts$/.test(file) || file.includes("/src/tests/")
25
+ }
26
+
27
+ /** A parameter property (`private readonly x: T`) wraps the parameter it declares. */
28
+ const unwrapParam = (param) => (param.type === "TSParameterProperty" ? param.parameter : param)
29
+
30
+ // -- TYPE-2 ----------------------------------------------------------------------------------------
31
+
32
+ /** A cast through `unknown` is the compiler being overruled twice. */
33
+ export const noDoubleCast = {
34
+ meta: {
35
+ type: "problem",
36
+ docs: { description: "No `x as unknown as T` outside the test lanes." },
37
+ schema: [],
38
+ messages: {
39
+ doubleCast:
40
+ "`as unknown as` is the compiler saying these types do not overlap, and being overruled twice. It is worse than `any` in one specific way: the result CLAIMS to be the target type, so everything downstream trusts it completely and the failure surfaces far from this line. Fix the type, or narrow with a guard that actually checks.",
41
+ },
42
+ },
43
+ create(context) {
44
+ if (isTestFile(context.filename || context.getFilename())) return {}
45
+ return {
46
+ TSAsExpression(node) {
47
+ // the OUTER cast of the pair: its operand is itself a cast, to `unknown`
48
+ const inner = node.expression
49
+ if (!inner || inner.type !== "TSAsExpression") return
50
+ const innerType = inner.typeAnnotation
51
+ if (!innerType || innerType.type !== "TSUnknownKeyword") return
52
+ context.report({ node, messageId: "doubleCast" })
53
+ },
54
+ }
55
+ },
56
+ }
57
+
58
+ // -- TYPE-3 ----------------------------------------------------------------------------------------
59
+
60
+ /** A destructured parameter's type is named, so a second caller can reference it. */
61
+ export const noInlineParamType = {
62
+ meta: {
63
+ type: "problem",
64
+ docs: { description: "A destructured parameter takes a named type, not an inline literal." },
65
+ schema: [],
66
+ messages: {
67
+ inline:
68
+ "An inline object type on a destructured parameter cannot be referenced, imported or extended - so the second caller writes it again, and when a third field arrives only one of the two copies gets it. Move it to a named interface in the module's `types/`.",
69
+ },
70
+ },
71
+ create(context) {
72
+ const check = (params) => {
73
+ for (const raw of params || []) {
74
+ const param = unwrapParam(raw)
75
+ if (param.type !== "ObjectPattern" || !param.typeAnnotation) continue
76
+ const annotation = param.typeAnnotation.typeAnnotation
77
+ if (annotation && annotation.type === "TSTypeLiteral") {
78
+ context.report({ node: param.typeAnnotation, messageId: "inline" })
79
+ }
80
+ }
81
+ }
82
+ return {
83
+ FunctionDeclaration(node) {
84
+ check(node.params)
85
+ },
86
+ FunctionExpression(node) {
87
+ check(node.params)
88
+ },
89
+ ArrowFunctionExpression(node) {
90
+ check(node.params)
91
+ },
92
+ }
93
+ },
94
+ }
95
+
96
+ // -- TYPE-4 ----------------------------------------------------------------------------------------
97
+
98
+ /** An enum keeps its runtime object. */
99
+ export const noConstEnum = {
100
+ meta: {
101
+ type: "problem",
102
+ docs: { description: "Enums are declared plain, never `const enum`." },
103
+ schema: [],
104
+ messages: {
105
+ constEnum:
106
+ "`const enum {{name}}` is inlined at compile time and has no runtime object: it cannot be iterated, cannot be reverse-mapped, and cannot cross the isolated-modules boundary this repository compiles under. It saves a few bytes and costs a family of things that simply do not work. Declare a plain `enum`.",
107
+ },
108
+ },
109
+ create(context) {
110
+ return {
111
+ TSEnumDeclaration(node) {
112
+ if (!node.const) return
113
+ context.report({ node, messageId: "constEnum", data: { name: node.id.name } })
114
+ },
115
+ }
116
+ },
117
+ }
118
+
119
+ /** The rules this law contributes to the plugin. */
120
+ export const rules = {
121
+ "no-double-cast": noDoubleCast,
122
+ "no-inline-param-type": noInlineParamType,
123
+ "no-const-enum": noConstEnum,
124
+ }
125
+
126
+ /**
127
+ * The level this law asks for, as the plugin's own opinion.
128
+ *
129
+ * The two standard rules are listed beside the house ones so a consuming repository switches the
130
+ * set on together -- `no-explicit-any` in particular is the one everybody already has and the one
131
+ * whose absence makes the rest decorative.
132
+ *
133
+ * The test exit for `no-double-cast` is inside the rule rather than in a config glob, because
134
+ * building a deliberately wrong value is how a spec proves a closed API refuses it, and that is a
135
+ * property of the lane rather than of any repository's file layout.
136
+ */
137
+ export const recommended = {
138
+ "starci-be/no-double-cast": "error",
139
+ "starci-be/no-inline-param-type": "error",
140
+ "starci-be/no-const-enum": "error",
141
+ "@typescript-eslint/no-explicit-any": "error",
142
+ "@typescript-eslint/array-type": ["error", {
143
+ default: "generic",
144
+ readonly: "generic",
145
+ }],
146
+ }