@formspec/eslint-plugin 0.1.0-alpha.3

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.
Files changed (50) hide show
  1. package/README.md +217 -0
  2. package/dist/index.d.ts +57 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +104 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/rules/decorator-field-type-mismatch.d.ts +15 -0
  7. package/dist/rules/decorator-field-type-mismatch.d.ts.map +1 -0
  8. package/dist/rules/decorator-field-type-mismatch.js +87 -0
  9. package/dist/rules/decorator-field-type-mismatch.js.map +1 -0
  10. package/dist/rules/enum-options-match-type.d.ts +26 -0
  11. package/dist/rules/enum-options-match-type.d.ts.map +1 -0
  12. package/dist/rules/enum-options-match-type.js +115 -0
  13. package/dist/rules/enum-options-match-type.js.map +1 -0
  14. package/dist/rules/index.d.ts +11 -0
  15. package/dist/rules/index.d.ts.map +1 -0
  16. package/dist/rules/index.js +11 -0
  17. package/dist/rules/index.js.map +1 -0
  18. package/dist/rules/min-max-valid-range.d.ts +26 -0
  19. package/dist/rules/min-max-valid-range.d.ts.map +1 -0
  20. package/dist/rules/min-max-valid-range.js +82 -0
  21. package/dist/rules/min-max-valid-range.js.map +1 -0
  22. package/dist/rules/no-conflicting-decorators.d.ts +19 -0
  23. package/dist/rules/no-conflicting-decorators.d.ts.map +1 -0
  24. package/dist/rules/no-conflicting-decorators.js +76 -0
  25. package/dist/rules/no-conflicting-decorators.js.map +1 -0
  26. package/dist/rules/no-duplicate-decorators.d.ts +19 -0
  27. package/dist/rules/no-duplicate-decorators.d.ts.map +1 -0
  28. package/dist/rules/no-duplicate-decorators.js +74 -0
  29. package/dist/rules/no-duplicate-decorators.js.map +1 -0
  30. package/dist/rules/showwhen-field-exists.d.ts +21 -0
  31. package/dist/rules/showwhen-field-exists.d.ts.map +1 -0
  32. package/dist/rules/showwhen-field-exists.js +68 -0
  33. package/dist/rules/showwhen-field-exists.js.map +1 -0
  34. package/dist/rules/showwhen-suggests-optional.d.ts +19 -0
  35. package/dist/rules/showwhen-suggests-optional.d.ts.map +1 -0
  36. package/dist/rules/showwhen-suggests-optional.js +53 -0
  37. package/dist/rules/showwhen-suggests-optional.js.map +1 -0
  38. package/dist/utils/decorator-utils.d.ts +103 -0
  39. package/dist/utils/decorator-utils.d.ts.map +1 -0
  40. package/dist/utils/decorator-utils.js +222 -0
  41. package/dist/utils/decorator-utils.js.map +1 -0
  42. package/dist/utils/index.d.ts +6 -0
  43. package/dist/utils/index.d.ts.map +1 -0
  44. package/dist/utils/index.js +6 -0
  45. package/dist/utils/index.js.map +1 -0
  46. package/dist/utils/type-utils.d.ts +82 -0
  47. package/dist/utils/type-utils.d.ts.map +1 -0
  48. package/dist/utils/type-utils.js +192 -0
  49. package/dist/utils/type-utils.js.map +1 -0
  50. package/package.json +48 -0
package/README.md ADDED
@@ -0,0 +1,217 @@
1
+ # @formspec/eslint-plugin
2
+
3
+ ESLint plugin for validating FormSpec decorator DSL usage in TypeScript projects. This plugin catches common mistakes by ensuring decorators match their field types and enforcing consistency rules.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install --save-dev @formspec/eslint-plugin
9
+ # or
10
+ pnpm add -D @formspec/eslint-plugin
11
+ ```
12
+
13
+ ## Requirements
14
+
15
+ - ESLint v9+ (flat config)
16
+ - TypeScript v5+
17
+
18
+ ## Usage
19
+
20
+ ### Recommended Configuration
21
+
22
+ Add the plugin to your `eslint.config.js`:
23
+
24
+ ```javascript
25
+ import formspec from "@formspec/eslint-plugin";
26
+
27
+ export default [
28
+ // ... other configs
29
+ ...formspec.configs.recommended,
30
+ ];
31
+ ```
32
+
33
+ ### Manual Configuration
34
+
35
+ ```javascript
36
+ import formspec from "@formspec/eslint-plugin";
37
+
38
+ export default [
39
+ {
40
+ plugins: {
41
+ "@formspec": formspec,
42
+ },
43
+ rules: {
44
+ "@formspec/decorator-field-type-mismatch": "error",
45
+ "@formspec/enum-options-match-type": "error",
46
+ "@formspec/showwhen-field-exists": "error",
47
+ "@formspec/showwhen-suggests-optional": "warn",
48
+ "@formspec/min-max-valid-range": "error",
49
+ "@formspec/no-conflicting-decorators": "error",
50
+ "@formspec/no-duplicate-decorators": "error",
51
+ },
52
+ },
53
+ ];
54
+ ```
55
+
56
+ ## Rules
57
+
58
+ | Rule | Description | Recommended | Strict |
59
+ |------|-------------|-------------|--------|
60
+ | [`decorator-field-type-mismatch`](#decorator-field-type-mismatch) | Ensures decorators are applied to fields with compatible types | error | error |
61
+ | [`enum-options-match-type`](#enum-options-match-type) | Ensures @EnumOptions values match the field's union type | error | error |
62
+ | [`showwhen-field-exists`](#showwhen-field-exists) | Ensures @ShowWhen references a field that exists | error | error |
63
+ | [`showwhen-suggests-optional`](#showwhen-suggests-optional) | Suggests @ShowWhen fields should be optional | warn | error |
64
+ | [`min-max-valid-range`](#min-max-valid-range) | Ensures @Min/@Max have valid ranges | error | error |
65
+ | [`no-conflicting-decorators`](#no-conflicting-decorators) | Prevents decorators that imply conflicting types | error | error |
66
+ | [`no-duplicate-decorators`](#no-duplicate-decorators) | Prevents duplicate decorators on the same field | error | error |
67
+
68
+ ### decorator-field-type-mismatch
69
+
70
+ Ensures FormSpec decorators are applied to fields with compatible types.
71
+
72
+ ```typescript
73
+ // Valid
74
+ @Min(0)
75
+ @Max(100)
76
+ age!: number;
77
+
78
+ @Placeholder("Enter name")
79
+ name!: string;
80
+
81
+ @MinItems(1)
82
+ @MaxItems(10)
83
+ items!: string[];
84
+
85
+ // Invalid - @Min requires number field
86
+ @Min(0)
87
+ name!: string; // Error: @Min can only be used on number fields
88
+ ```
89
+
90
+ ### enum-options-match-type
91
+
92
+ Ensures @EnumOptions values match the field's TypeScript union type.
93
+
94
+ ```typescript
95
+ // Valid - options match type
96
+ @EnumOptions(["draft", "published", "archived"])
97
+ status!: "draft" | "published" | "archived";
98
+
99
+ // Valid - object options with id property
100
+ @EnumOptions([{ id: "a", label: "Option A" }, { id: "b", label: "Option B" }])
101
+ type!: "a" | "b";
102
+
103
+ // Valid - string type accepts any options
104
+ @EnumOptions(["any", "options"])
105
+ value!: string;
106
+
107
+ // Invalid - missing option "archived"
108
+ @EnumOptions(["draft", "published"])
109
+ status!: "draft" | "published" | "archived"; // Error: missing "archived"
110
+ ```
111
+
112
+ ### showwhen-field-exists
113
+
114
+ Ensures @ShowWhen references a field that exists in the same class.
115
+
116
+ ```typescript
117
+ // Valid
118
+ @EnumOptions(["a", "b"])
119
+ type!: "a" | "b";
120
+
121
+ @ShowWhen({ _predicate: "equals", field: "type", value: "a" })
122
+ conditionalField?: string;
123
+
124
+ // Invalid - "nonexistent" field doesn't exist
125
+ @ShowWhen({ _predicate: "equals", field: "nonexistent", value: "x" })
126
+ conditionalField?: string; // Error: field "nonexistent" does not exist
127
+ ```
128
+
129
+ ### showwhen-suggests-optional
130
+
131
+ Suggests that fields with @ShowWhen should be marked as optional since they may not be present in the output.
132
+
133
+ ```typescript
134
+ // Valid
135
+ @ShowWhen({ _predicate: "equals", field: "type", value: "a" })
136
+ conditionalField?: string; // Good - optional
137
+
138
+ // Warning
139
+ @ShowWhen({ _predicate: "equals", field: "type", value: "a" })
140
+ conditionalField!: string; // Warning: should be optional
141
+ ```
142
+
143
+ ### min-max-valid-range
144
+
145
+ Ensures @Min value is less than or equal to @Max value.
146
+
147
+ ```typescript
148
+ // Valid
149
+ @Min(0)
150
+ @Max(100)
151
+ value!: number;
152
+
153
+ @Min(5)
154
+ @Max(5) // Equal is valid
155
+ exact!: number;
156
+
157
+ // Invalid
158
+ @Min(100)
159
+ @Max(50) // Error: @Min(100) > @Max(50)
160
+ invalid!: number;
161
+ ```
162
+
163
+ ### no-conflicting-decorators
164
+
165
+ Prevents using decorators that imply conflicting field types.
166
+
167
+ ```typescript
168
+ // Valid - both imply number
169
+ @Min(0)
170
+ @Max(100)
171
+ value!: number;
172
+
173
+ // Invalid - @Min implies number, @Placeholder implies string
174
+ @Min(0)
175
+ @Placeholder("Enter value") // Error: conflicting decorators
176
+ field!: string;
177
+ ```
178
+
179
+ ### no-duplicate-decorators
180
+
181
+ Prevents applying the same decorator multiple times to a field.
182
+
183
+ ```typescript
184
+ // Valid
185
+ @Label("Name")
186
+ @Placeholder("Enter name")
187
+ name!: string;
188
+
189
+ // Invalid
190
+ @Label("First")
191
+ @Label("Second") // Error: duplicate @Label
192
+ name!: string;
193
+ ```
194
+
195
+ ## Configurations
196
+
197
+ ### Recommended
198
+
199
+ Sensible defaults for most projects:
200
+ - All type safety rules enabled as errors
201
+ - `showwhen-suggests-optional` as warning (not blocking)
202
+
203
+ ### Strict
204
+
205
+ All rules enabled as errors for maximum type safety enforcement.
206
+
207
+ ```javascript
208
+ import formspec from "@formspec/eslint-plugin";
209
+
210
+ export default [
211
+ ...formspec.configs.strict,
212
+ ];
213
+ ```
214
+
215
+ ## License
216
+
217
+ UNLICENSED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @formspec/eslint-plugin
3
+ *
4
+ * ESLint plugin for FormSpec decorator DSL type safety.
5
+ *
6
+ * Provides rules to catch common mistakes when using FormSpec decorators:
7
+ * - Type mismatches between decorators and field types
8
+ * - Invalid enum option configurations
9
+ * - Missing or invalid @ShowWhen references
10
+ * - Conflicting or duplicate decorators
11
+ */
12
+ import type { TSESLint } from "@typescript-eslint/utils";
13
+ import { decoratorFieldTypeMismatch } from "./rules/decorator-field-type-mismatch.js";
14
+ import { enumOptionsMatchType } from "./rules/enum-options-match-type.js";
15
+ import { showwhenFieldExists } from "./rules/showwhen-field-exists.js";
16
+ import { showwhenSuggestsOptional } from "./rules/showwhen-suggests-optional.js";
17
+ import { minMaxValidRange } from "./rules/min-max-valid-range.js";
18
+ import { noConflictingDecorators } from "./rules/no-conflicting-decorators.js";
19
+ import { noDuplicateDecorators } from "./rules/no-duplicate-decorators.js";
20
+ /**
21
+ * The FormSpec ESLint plugin.
22
+ */
23
+ declare const plugin: {
24
+ meta: {
25
+ name: string;
26
+ };
27
+ rules: {
28
+ readonly "decorator-field-type-mismatch": TSESLint.RuleModule<"minMaxOnNonNumber" | "minMaxItemsOnNonArray" | "placeholderOnNonString", [], unknown, TSESLint.RuleListener> & {
29
+ name: string;
30
+ };
31
+ readonly "enum-options-match-type": TSESLint.RuleModule<"enumOptionsMismatch" | "enumOptionsMissing" | "enumOptionsExtra", [], unknown, TSESLint.RuleListener> & {
32
+ name: string;
33
+ };
34
+ readonly "showwhen-field-exists": TSESLint.RuleModule<"fieldDoesNotExist", [], unknown, TSESLint.RuleListener> & {
35
+ name: string;
36
+ };
37
+ readonly "showwhen-suggests-optional": TSESLint.RuleModule<"shouldBeOptional", [], unknown, TSESLint.RuleListener> & {
38
+ name: string;
39
+ };
40
+ readonly "min-max-valid-range": TSESLint.RuleModule<"minGreaterThanMax" | "minItemsGreaterThanMaxItems", [], unknown, TSESLint.RuleListener> & {
41
+ name: string;
42
+ };
43
+ readonly "no-conflicting-decorators": TSESLint.RuleModule<"conflictingDecorators", [], unknown, TSESLint.RuleListener> & {
44
+ name: string;
45
+ };
46
+ readonly "no-duplicate-decorators": TSESLint.RuleModule<"duplicateDecorator", [], unknown, TSESLint.RuleListener> & {
47
+ name: string;
48
+ };
49
+ };
50
+ configs: {
51
+ recommended: TSESLint.FlatConfig.ConfigArray;
52
+ strict: TSESLint.FlatConfig.ConfigArray;
53
+ };
54
+ };
55
+ export default plugin;
56
+ export { decoratorFieldTypeMismatch, enumOptionsMatchType, showwhenFieldExists, showwhenSuggestsOptional, minMaxValidRange, noConflictingDecorators, noDuplicateDecorators, };
57
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,0BAA0B,EAAE,MAAM,0CAA0C,CAAC;AACtF,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AACjF,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AA6E3E;;GAEG;AACH,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAOX,CAAC;AAEF,eAAe,MAAM,CAAC;AAGtB,OAAO,EACL,0BAA0B,EAC1B,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,gBAAgB,EAChB,uBAAuB,EACvB,qBAAqB,GACtB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @formspec/eslint-plugin
3
+ *
4
+ * ESLint plugin for FormSpec decorator DSL type safety.
5
+ *
6
+ * Provides rules to catch common mistakes when using FormSpec decorators:
7
+ * - Type mismatches between decorators and field types
8
+ * - Invalid enum option configurations
9
+ * - Missing or invalid @ShowWhen references
10
+ * - Conflicting or duplicate decorators
11
+ */
12
+ import { decoratorFieldTypeMismatch } from "./rules/decorator-field-type-mismatch.js";
13
+ import { enumOptionsMatchType } from "./rules/enum-options-match-type.js";
14
+ import { showwhenFieldExists } from "./rules/showwhen-field-exists.js";
15
+ import { showwhenSuggestsOptional } from "./rules/showwhen-suggests-optional.js";
16
+ import { minMaxValidRange } from "./rules/min-max-valid-range.js";
17
+ import { noConflictingDecorators } from "./rules/no-conflicting-decorators.js";
18
+ import { noDuplicateDecorators } from "./rules/no-duplicate-decorators.js";
19
+ /**
20
+ * All rules provided by this plugin.
21
+ */
22
+ const rules = {
23
+ "decorator-field-type-mismatch": decoratorFieldTypeMismatch,
24
+ "enum-options-match-type": enumOptionsMatchType,
25
+ "showwhen-field-exists": showwhenFieldExists,
26
+ "showwhen-suggests-optional": showwhenSuggestsOptional,
27
+ "min-max-valid-range": minMaxValidRange,
28
+ "no-conflicting-decorators": noConflictingDecorators,
29
+ "no-duplicate-decorators": noDuplicateDecorators,
30
+ };
31
+ /**
32
+ * Plugin metadata.
33
+ */
34
+ const meta = {
35
+ name: "@formspec/eslint-plugin",
36
+ };
37
+ /**
38
+ * Recommended configuration for FormSpec projects.
39
+ *
40
+ * Usage:
41
+ * ```javascript
42
+ * import formspec from "@formspec/eslint-plugin";
43
+ *
44
+ * export default [
45
+ * ...formspec.configs.recommended,
46
+ * ];
47
+ * ```
48
+ */
49
+ const recommendedConfig = [
50
+ {
51
+ plugins: {
52
+ "@formspec": {
53
+ meta,
54
+ rules,
55
+ },
56
+ },
57
+ rules: {
58
+ "@formspec/decorator-field-type-mismatch": "error",
59
+ "@formspec/enum-options-match-type": "error",
60
+ "@formspec/showwhen-field-exists": "error",
61
+ "@formspec/showwhen-suggests-optional": "warn",
62
+ "@formspec/min-max-valid-range": "error",
63
+ "@formspec/no-conflicting-decorators": "error",
64
+ "@formspec/no-duplicate-decorators": "error",
65
+ },
66
+ },
67
+ ];
68
+ /**
69
+ * Strict configuration - all rules as errors.
70
+ */
71
+ const strictConfig = [
72
+ {
73
+ plugins: {
74
+ "@formspec": {
75
+ meta,
76
+ rules,
77
+ },
78
+ },
79
+ rules: {
80
+ "@formspec/decorator-field-type-mismatch": "error",
81
+ "@formspec/enum-options-match-type": "error",
82
+ "@formspec/showwhen-field-exists": "error",
83
+ "@formspec/showwhen-suggests-optional": "error",
84
+ "@formspec/min-max-valid-range": "error",
85
+ "@formspec/no-conflicting-decorators": "error",
86
+ "@formspec/no-duplicate-decorators": "error",
87
+ },
88
+ },
89
+ ];
90
+ /**
91
+ * The FormSpec ESLint plugin.
92
+ */
93
+ const plugin = {
94
+ meta,
95
+ rules,
96
+ configs: {
97
+ recommended: recommendedConfig,
98
+ strict: strictConfig,
99
+ },
100
+ };
101
+ export default plugin;
102
+ // Named exports for individual rules
103
+ export { decoratorFieldTypeMismatch, enumOptionsMatchType, showwhenFieldExists, showwhenSuggestsOptional, minMaxValidRange, noConflictingDecorators, noDuplicateDecorators, };
104
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,0BAA0B,EAAE,MAAM,0CAA0C,CAAC;AACtF,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AACjF,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAE3E;;GAEG;AACH,MAAM,KAAK,GAAG;IACZ,+BAA+B,EAAE,0BAA0B;IAC3D,yBAAyB,EAAE,oBAAoB;IAC/C,uBAAuB,EAAE,mBAAmB;IAC5C,4BAA4B,EAAE,wBAAwB;IACtD,qBAAqB,EAAE,gBAAgB;IACvC,2BAA2B,EAAE,uBAAuB;IACpD,yBAAyB,EAAE,qBAAqB;CACxC,CAAC;AAEX;;GAEG;AACH,MAAM,IAAI,GAAG;IACX,IAAI,EAAE,yBAAyB;CAChC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,iBAAiB,GAAoC;IACzD;QACE,OAAO,EAAE;YACP,WAAW,EAAE;gBACX,IAAI;gBACJ,KAAK;aACN;SACF;QACD,KAAK,EAAE;YACL,yCAAyC,EAAE,OAAO;YAClD,mCAAmC,EAAE,OAAO;YAC5C,iCAAiC,EAAE,OAAO;YAC1C,sCAAsC,EAAE,MAAM;YAC9C,+BAA+B,EAAE,OAAO;YACxC,qCAAqC,EAAE,OAAO;YAC9C,mCAAmC,EAAE,OAAO;SAC7C;KACF;CACF,CAAC;AAEF;;GAEG;AACH,MAAM,YAAY,GAAoC;IACpD;QACE,OAAO,EAAE;YACP,WAAW,EAAE;gBACX,IAAI;gBACJ,KAAK;aACN;SACF;QACD,KAAK,EAAE;YACL,yCAAyC,EAAE,OAAO;YAClD,mCAAmC,EAAE,OAAO;YAC5C,iCAAiC,EAAE,OAAO;YAC1C,sCAAsC,EAAE,OAAO;YAC/C,+BAA+B,EAAE,OAAO;YACxC,qCAAqC,EAAE,OAAO;YAC9C,mCAAmC,EAAE,OAAO;SAC7C;KACF;CACF,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,GAAG;IACb,IAAI;IACJ,KAAK;IACL,OAAO,EAAE;QACP,WAAW,EAAE,iBAAiB;QAC9B,MAAM,EAAE,YAAY;KACrB;CACF,CAAC;AAEF,eAAe,MAAM,CAAC;AAEtB,qCAAqC;AACrC,OAAO,EACL,0BAA0B,EAC1B,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,gBAAgB,EAChB,uBAAuB,EACvB,qBAAqB,GACtB,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Rule: decorator-field-type-mismatch
3
+ *
4
+ * Ensures FormSpec decorators are applied to fields with compatible types:
5
+ * - @Min/@Max only on number fields
6
+ * - @MinItems/@MaxItems only on array fields
7
+ * - @Placeholder only on string fields
8
+ */
9
+ import { ESLintUtils } from "@typescript-eslint/utils";
10
+ type MessageIds = "minMaxOnNonNumber" | "minMaxItemsOnNonArray" | "placeholderOnNonString";
11
+ export declare const decoratorFieldTypeMismatch: ESLintUtils.RuleModule<MessageIds, [], unknown, ESLintUtils.RuleListener> & {
12
+ name: string;
13
+ };
14
+ export {};
15
+ //# sourceMappingURL=decorator-field-type-mismatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorator-field-type-mismatch.d.ts","sourceRoot":"","sources":["../../src/rules/decorator-field-type-mismatch.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAkB,MAAM,0BAA0B,CAAC;AAgBvE,KAAK,UAAU,GACX,mBAAmB,GACnB,uBAAuB,GACvB,wBAAwB,CAAC;AAmB7B,eAAO,MAAM,0BAA0B;;CAoErC,CAAC"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Rule: decorator-field-type-mismatch
3
+ *
4
+ * Ensures FormSpec decorators are applied to fields with compatible types:
5
+ * - @Min/@Max only on number fields
6
+ * - @MinItems/@MaxItems only on array fields
7
+ * - @Placeholder only on string fields
8
+ */
9
+ import { ESLintUtils, AST_NODE_TYPES } from "@typescript-eslint/utils";
10
+ import { getFormSpecDecorators, DECORATOR_TYPE_HINTS, } from "../utils/decorator-utils.js";
11
+ import { getPropertyType, getFieldTypeCategory, } from "../utils/type-utils.js";
12
+ const createRule = ESLintUtils.RuleCreator((name) => `https://formspec.dev/eslint-plugin/rules/${name}`);
13
+ const EXPECTED_TYPES = {
14
+ Min: ["number"],
15
+ Max: ["number"],
16
+ Placeholder: ["string"],
17
+ MinItems: ["array"],
18
+ MaxItems: ["array"],
19
+ EnumOptions: ["string", "union"], // Handled by separate rule
20
+ };
21
+ const DECORATOR_MESSAGE_IDS = {
22
+ Min: "minMaxOnNonNumber",
23
+ Max: "minMaxOnNonNumber",
24
+ Placeholder: "placeholderOnNonString",
25
+ MinItems: "minMaxItemsOnNonArray",
26
+ MaxItems: "minMaxItemsOnNonArray",
27
+ };
28
+ export const decoratorFieldTypeMismatch = createRule({
29
+ name: "decorator-field-type-mismatch",
30
+ meta: {
31
+ type: "problem",
32
+ docs: {
33
+ description: "Ensures FormSpec decorators are applied to fields with compatible types",
34
+ },
35
+ messages: {
36
+ minMaxOnNonNumber: "@{{decorator}} can only be used on number fields, but field '{{field}}' has type '{{actualType}}'",
37
+ minMaxItemsOnNonArray: "@{{decorator}} can only be used on array fields, but field '{{field}}' has type '{{actualType}}'",
38
+ placeholderOnNonString: "@Placeholder can only be used on string fields, but field '{{field}}' has type '{{actualType}}'",
39
+ },
40
+ schema: [],
41
+ },
42
+ defaultOptions: [],
43
+ create(context) {
44
+ const services = ESLintUtils.getParserServices(context);
45
+ const checker = services.program.getTypeChecker();
46
+ return {
47
+ PropertyDefinition(node) {
48
+ const decorators = getFormSpecDecorators(node);
49
+ if (decorators.length === 0)
50
+ return;
51
+ const type = getPropertyType(node, services);
52
+ if (!type)
53
+ return;
54
+ const fieldTypeCategory = getFieldTypeCategory(type, checker);
55
+ const fieldName = node.key.type === AST_NODE_TYPES.Identifier ? node.key.name : "<computed>";
56
+ const actualType = checker.typeToString(type);
57
+ for (const decorator of decorators) {
58
+ const decoratorName = decorator.name;
59
+ // Skip decorators that don't have type hints
60
+ if (!(decoratorName in DECORATOR_TYPE_HINTS))
61
+ continue;
62
+ // Skip EnumOptions - handled by separate rule
63
+ if (decoratorName === "EnumOptions")
64
+ continue;
65
+ const expectedTypes = EXPECTED_TYPES[decoratorName];
66
+ // Check if field type matches any expected type
67
+ const isCompatible = expectedTypes.includes(fieldTypeCategory);
68
+ if (!isCompatible) {
69
+ const messageId = DECORATOR_MESSAGE_IDS[decoratorName];
70
+ if (!messageId)
71
+ continue;
72
+ context.report({
73
+ node: decorator.node,
74
+ messageId,
75
+ data: {
76
+ decorator: decoratorName,
77
+ field: fieldName,
78
+ actualType,
79
+ },
80
+ });
81
+ }
82
+ }
83
+ },
84
+ };
85
+ },
86
+ });
87
+ //# sourceMappingURL=decorator-field-type-mismatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorator-field-type-mismatch.js","sourceRoot":"","sources":["../../src/rules/decorator-field-type-mismatch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Rule: enum-options-match-type
3
+ *
4
+ * Ensures @EnumOptions values match the field's TypeScript type.
5
+ *
6
+ * Valid:
7
+ * @EnumOptions(["a", "b", "c"])
8
+ * field!: "a" | "b" | "c";
9
+ *
10
+ * @EnumOptions([{ id: "x", label: "X" }])
11
+ * field!: "x";
12
+ *
13
+ * @EnumOptions(["a", "b"])
14
+ * field!: string; // Permissive - string accepts any enum
15
+ *
16
+ * Invalid:
17
+ * @EnumOptions(["a", "b", "c"])
18
+ * field!: "x" | "y"; // Mismatch
19
+ */
20
+ import { ESLintUtils } from "@typescript-eslint/utils";
21
+ type MessageIds = "enumOptionsMismatch" | "enumOptionsMissing" | "enumOptionsExtra";
22
+ export declare const enumOptionsMatchType: ESLintUtils.RuleModule<MessageIds, [], unknown, ESLintUtils.RuleListener> & {
23
+ name: string;
24
+ };
25
+ export {};
26
+ //# sourceMappingURL=enum-options-match-type.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enum-options-match-type.d.ts","sourceRoot":"","sources":["../../src/rules/enum-options-match-type.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAevD,KAAK,UAAU,GAAG,qBAAqB,GAAG,oBAAoB,GAAG,kBAAkB,CAAC;AAEpF,eAAO,MAAM,oBAAoB;;CAqG/B,CAAC"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Rule: enum-options-match-type
3
+ *
4
+ * Ensures @EnumOptions values match the field's TypeScript type.
5
+ *
6
+ * Valid:
7
+ * @EnumOptions(["a", "b", "c"])
8
+ * field!: "a" | "b" | "c";
9
+ *
10
+ * @EnumOptions([{ id: "x", label: "X" }])
11
+ * field!: "x";
12
+ *
13
+ * @EnumOptions(["a", "b"])
14
+ * field!: string; // Permissive - string accepts any enum
15
+ *
16
+ * Invalid:
17
+ * @EnumOptions(["a", "b", "c"])
18
+ * field!: "x" | "y"; // Mismatch
19
+ */
20
+ import { ESLintUtils } from "@typescript-eslint/utils";
21
+ import { findDecorator, getDecoratorArrayArg, } from "../utils/decorator-utils.js";
22
+ import { getPropertyType, getStringLiteralUnionValues, isStringType, } from "../utils/type-utils.js";
23
+ const createRule = ESLintUtils.RuleCreator((name) => `https://formspec.dev/eslint-plugin/rules/${name}`);
24
+ export const enumOptionsMatchType = createRule({
25
+ name: "enum-options-match-type",
26
+ meta: {
27
+ type: "problem",
28
+ docs: {
29
+ description: "Ensures @EnumOptions values match the field's TypeScript union type",
30
+ },
31
+ messages: {
32
+ enumOptionsMismatch: "@EnumOptions values don't match field type. Options: [{{options}}], Type: {{fieldType}}",
33
+ enumOptionsMissing: "@EnumOptions is missing values that are in the field type: [{{missing}}]",
34
+ enumOptionsExtra: "@EnumOptions has values not in the field type: [{{extra}}]",
35
+ },
36
+ schema: [],
37
+ },
38
+ defaultOptions: [],
39
+ create(context) {
40
+ const services = ESLintUtils.getParserServices(context);
41
+ const checker = services.program.getTypeChecker();
42
+ return {
43
+ PropertyDefinition(node) {
44
+ const enumOptionsDecorator = findDecorator(node, "EnumOptions");
45
+ if (!enumOptionsDecorator)
46
+ return;
47
+ const type = getPropertyType(node, services);
48
+ if (!type)
49
+ return;
50
+ // If field type is plain `string`, any enum options are valid
51
+ if (isStringType(type, checker) && !type.isUnion()) {
52
+ return;
53
+ }
54
+ // Get the enum option values from the decorator
55
+ const decoratorValues = getDecoratorArrayArg(enumOptionsDecorator);
56
+ if (!decoratorValues)
57
+ return;
58
+ // Extract IDs from decorator options
59
+ const optionIds = new Set();
60
+ for (const value of decoratorValues) {
61
+ if (typeof value === "string") {
62
+ optionIds.add(value);
63
+ }
64
+ else if (typeof value === "object" && value !== null && "id" in value) {
65
+ const id = value.id;
66
+ if (typeof id === "string") {
67
+ optionIds.add(id);
68
+ }
69
+ }
70
+ }
71
+ // Get the union type values from the field type
72
+ const typeValues = getStringLiteralUnionValues(type, checker);
73
+ if (!typeValues) {
74
+ // Field type is not a string literal union - can't compare
75
+ return;
76
+ }
77
+ const typeSet = new Set(typeValues);
78
+ // Find missing values (in type but not in options)
79
+ const missing = [];
80
+ for (const v of typeValues) {
81
+ if (!optionIds.has(v)) {
82
+ missing.push(v);
83
+ }
84
+ }
85
+ // Find extra values (in options but not in type)
86
+ const extra = [];
87
+ for (const v of optionIds) {
88
+ if (!typeSet.has(v)) {
89
+ extra.push(v);
90
+ }
91
+ }
92
+ // Report specific issues
93
+ if (missing.length > 0) {
94
+ context.report({
95
+ node: enumOptionsDecorator.node,
96
+ messageId: "enumOptionsMissing",
97
+ data: {
98
+ missing: missing.map((v) => `"${v}"`).join(", "),
99
+ },
100
+ });
101
+ }
102
+ if (extra.length > 0) {
103
+ context.report({
104
+ node: enumOptionsDecorator.node,
105
+ messageId: "enumOptionsExtra",
106
+ data: {
107
+ extra: extra.map((v) => `"${v}"`).join(", "),
108
+ },
109
+ });
110
+ }
111
+ },
112
+ };
113
+ },
114
+ });
115
+ //# sourceMappingURL=enum-options-match-type.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enum-options-match-type.js","sourceRoot":"","sources":["../../src/rules/enum-options-match-type.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * All FormSpec ESLint rules.
3
+ */
4
+ export { decoratorFieldTypeMismatch } from "./decorator-field-type-mismatch.js";
5
+ export { enumOptionsMatchType } from "./enum-options-match-type.js";
6
+ export { showwhenFieldExists } from "./showwhen-field-exists.js";
7
+ export { showwhenSuggestsOptional } from "./showwhen-suggests-optional.js";
8
+ export { minMaxValidRange } from "./min-max-valid-range.js";
9
+ export { noConflictingDecorators } from "./no-conflicting-decorators.js";
10
+ export { noDuplicateDecorators } from "./no-duplicate-decorators.js";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAChF,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC"}