@formspec/eslint-plugin 0.1.0-alpha.10
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 +237 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +117 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/consistent-constraints.d.ts +22 -0
- package/dist/rules/consistent-constraints.d.ts.map +1 -0
- package/dist/rules/consistent-constraints.js +178 -0
- package/dist/rules/consistent-constraints.js.map +1 -0
- package/dist/rules/constraints/allowed-field-types.d.ts +16 -0
- package/dist/rules/constraints/allowed-field-types.d.ts.map +1 -0
- package/dist/rules/constraints/allowed-field-types.js +133 -0
- package/dist/rules/constraints/allowed-field-types.js.map +1 -0
- package/dist/rules/constraints/allowed-layouts.d.ts +17 -0
- package/dist/rules/constraints/allowed-layouts.d.ts.map +1 -0
- package/dist/rules/constraints/allowed-layouts.js +83 -0
- package/dist/rules/constraints/allowed-layouts.js.map +1 -0
- package/dist/rules/constraints/index.d.ts +9 -0
- package/dist/rules/constraints/index.d.ts.map +1 -0
- package/dist/rules/constraints/index.js +9 -0
- package/dist/rules/constraints/index.js.map +1 -0
- package/dist/rules/decorator-allowed-field-types.d.ts +17 -0
- package/dist/rules/decorator-allowed-field-types.d.ts.map +1 -0
- package/dist/rules/decorator-allowed-field-types.js +71 -0
- package/dist/rules/decorator-allowed-field-types.js.map +1 -0
- package/dist/rules/decorator-field-type-mismatch.d.ts +14 -0
- package/dist/rules/decorator-field-type-mismatch.d.ts.map +1 -0
- package/dist/rules/decorator-field-type-mismatch.js +116 -0
- package/dist/rules/decorator-field-type-mismatch.js.map +1 -0
- package/dist/rules/enum-options-match-type.d.ts +26 -0
- package/dist/rules/enum-options-match-type.d.ts.map +1 -0
- package/dist/rules/enum-options-match-type.js +115 -0
- package/dist/rules/enum-options-match-type.js.map +1 -0
- package/dist/rules/index.d.ts +13 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +13 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/no-conflicting-decorators.d.ts +15 -0
- package/dist/rules/no-conflicting-decorators.d.ts.map +1 -0
- package/dist/rules/no-conflicting-decorators.js +72 -0
- package/dist/rules/no-conflicting-decorators.js.map +1 -0
- package/dist/rules/no-duplicate-decorators.d.ts +19 -0
- package/dist/rules/no-duplicate-decorators.d.ts.map +1 -0
- package/dist/rules/no-duplicate-decorators.js +59 -0
- package/dist/rules/no-duplicate-decorators.js.map +1 -0
- package/dist/rules/prefer-custom-decorator.d.ts +22 -0
- package/dist/rules/prefer-custom-decorator.d.ts.map +1 -0
- package/dist/rules/prefer-custom-decorator.js +72 -0
- package/dist/rules/prefer-custom-decorator.js.map +1 -0
- package/dist/rules/showwhen-field-exists.d.ts +21 -0
- package/dist/rules/showwhen-field-exists.d.ts.map +1 -0
- package/dist/rules/showwhen-field-exists.js +68 -0
- package/dist/rules/showwhen-field-exists.js.map +1 -0
- package/dist/rules/showwhen-suggests-optional.d.ts +19 -0
- package/dist/rules/showwhen-suggests-optional.d.ts.map +1 -0
- package/dist/rules/showwhen-suggests-optional.js +53 -0
- package/dist/rules/showwhen-suggests-optional.js.map +1 -0
- package/dist/utils/decorator-utils.d.ts +105 -0
- package/dist/utils/decorator-utils.d.ts.map +1 -0
- package/dist/utils/decorator-utils.js +216 -0
- package/dist/utils/decorator-utils.js.map +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/jsdoc-utils.d.ts +31 -0
- package/dist/utils/jsdoc-utils.d.ts.map +1 -0
- package/dist/utils/jsdoc-utils.js +81 -0
- package/dist/utils/jsdoc-utils.js.map +1 -0
- package/dist/utils/type-utils.d.ts +82 -0
- package/dist/utils/type-utils.d.ts.map +1 -0
- package/dist/utils/type-utils.js +216 -0
- package/dist/utils/type-utils.js.map +1 -0
- package/package.json +50 -0
|
@@ -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,13 @@
|
|
|
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 { consistentConstraints } from "./consistent-constraints.js";
|
|
9
|
+
export { noConflictingDecorators } from "./no-conflicting-decorators.js";
|
|
10
|
+
export { noDuplicateDecorators } from "./no-duplicate-decorators.js";
|
|
11
|
+
export { decoratorAllowedFieldTypes } from "./decorator-allowed-field-types.js";
|
|
12
|
+
export { preferCustomDecorator } from "./prefer-custom-decorator.js";
|
|
13
|
+
//# 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,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
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 { consistentConstraints } from "./consistent-constraints.js";
|
|
9
|
+
export { noConflictingDecorators } from "./no-conflicting-decorators.js";
|
|
10
|
+
export { noDuplicateDecorators } from "./no-duplicate-decorators.js";
|
|
11
|
+
export { decoratorAllowedFieldTypes } from "./decorator-allowed-field-types.js";
|
|
12
|
+
export { preferCustomDecorator } from "./prefer-custom-decorator.js";
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","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,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: no-conflicting-decorators
|
|
3
|
+
*
|
|
4
|
+
* Ensures a field doesn't have decorators that imply conflicting types.
|
|
5
|
+
*
|
|
6
|
+
* Invalid:
|
|
7
|
+
* @Minimum(0) // Implies number
|
|
8
|
+
* @MinLength(1) // Implies string
|
|
9
|
+
* field!: string;
|
|
10
|
+
*/
|
|
11
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
12
|
+
export declare const noConflictingDecorators: ESLintUtils.RuleModule<"conflictingDecorators", [], unknown, ESLintUtils.RuleListener> & {
|
|
13
|
+
name: string;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=no-conflicting-decorators.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-conflicting-decorators.d.ts","sourceRoot":"","sources":["../../src/rules/no-conflicting-decorators.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAavD,eAAO,MAAM,uBAAuB;;CA4DlC,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: no-conflicting-decorators
|
|
3
|
+
*
|
|
4
|
+
* Ensures a field doesn't have decorators that imply conflicting types.
|
|
5
|
+
*
|
|
6
|
+
* Invalid:
|
|
7
|
+
* @Minimum(0) // Implies number
|
|
8
|
+
* @MinLength(1) // Implies string
|
|
9
|
+
* field!: string;
|
|
10
|
+
*/
|
|
11
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
12
|
+
import { getFormSpecDecorators, DECORATOR_TYPE_HINTS, } from "../utils/decorator-utils.js";
|
|
13
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://formspec.dev/eslint-plugin/rules/${name}`);
|
|
14
|
+
export const noConflictingDecorators = createRule({
|
|
15
|
+
name: "no-conflicting-decorators",
|
|
16
|
+
meta: {
|
|
17
|
+
type: "problem",
|
|
18
|
+
docs: {
|
|
19
|
+
description: "Ensures a field doesn't have decorators that imply conflicting types",
|
|
20
|
+
},
|
|
21
|
+
messages: {
|
|
22
|
+
conflictingDecorators: "Field has conflicting decorators: @{{decorator1}} implies {{type1}}, but @{{decorator2}} implies {{type2}}",
|
|
23
|
+
},
|
|
24
|
+
schema: [],
|
|
25
|
+
},
|
|
26
|
+
defaultOptions: [],
|
|
27
|
+
create(context) {
|
|
28
|
+
return {
|
|
29
|
+
PropertyDefinition(node) {
|
|
30
|
+
const decorators = getFormSpecDecorators(node);
|
|
31
|
+
if (decorators.length < 2)
|
|
32
|
+
return;
|
|
33
|
+
// Collect type hints from decorators
|
|
34
|
+
const typeHints = [];
|
|
35
|
+
for (const decorator of decorators) {
|
|
36
|
+
// Only process decorators that have type hints
|
|
37
|
+
if (decorator.name in DECORATOR_TYPE_HINTS) {
|
|
38
|
+
const decoratorName = decorator.name;
|
|
39
|
+
const typeHint = DECORATOR_TYPE_HINTS[decoratorName];
|
|
40
|
+
typeHints.push({ decorator: decoratorName, type: typeHint });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Check for conflicts
|
|
44
|
+
if (typeHints.length < 2)
|
|
45
|
+
return;
|
|
46
|
+
const firstHint = typeHints[0];
|
|
47
|
+
if (!firstHint)
|
|
48
|
+
return;
|
|
49
|
+
for (let i = 1; i < typeHints.length; i++) {
|
|
50
|
+
const hint = typeHints[i];
|
|
51
|
+
if (!hint)
|
|
52
|
+
continue;
|
|
53
|
+
if (hint.type !== firstHint.type) {
|
|
54
|
+
context.report({
|
|
55
|
+
node: node.key,
|
|
56
|
+
messageId: "conflictingDecorators",
|
|
57
|
+
data: {
|
|
58
|
+
decorator1: firstHint.decorator,
|
|
59
|
+
type1: firstHint.type,
|
|
60
|
+
decorator2: hint.decorator,
|
|
61
|
+
type2: hint.type,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
// Only report once per field
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
//# sourceMappingURL=no-conflicting-decorators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-conflicting-decorators.js","sourceRoot":"","sources":["../../src/rules/no-conflicting-decorators.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EACL,qBAAqB,EACrB,oBAAoB,GAErB,MAAM,6BAA6B,CAAC;AAErC,MAAM,UAAU,GAAG,WAAW,CAAC,WAAW,CACxC,CAAC,IAAI,EAAE,EAAE,CAAC,4CAA4C,IAAI,EAAE,CAC7D,CAAC;AAIF,MAAM,CAAC,MAAM,uBAAuB,GAAG,UAAU,CAAiB;IAChE,IAAI,EAAE,2BAA2B;IACjC,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EAAE,sEAAsE;SACpF;QACD,QAAQ,EAAE;YACR,qBAAqB,EACnB,4GAA4G;SAC/G;QACD,MAAM,EAAE,EAAE;KACX;IACD,cAAc,EAAE,EAAE;IAClB,MAAM,CAAC,OAAO;QACZ,OAAO;YACL,kBAAkB,CAAC,IAAI;gBACrB,MAAM,UAAU,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;gBAC/C,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;oBAAE,OAAO;gBAElC,qCAAqC;gBACrC,MAAM,SAAS,GAA0C,EAAE,CAAC;gBAE5D,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;oBACnC,+CAA+C;oBAC/C,IAAI,SAAS,CAAC,IAAI,IAAI,oBAAoB,EAAE,CAAC;wBAC3C,MAAM,aAAa,GAAG,SAAS,CAAC,IAAyB,CAAC;wBAC1D,MAAM,QAAQ,GAAG,oBAAoB,CAAC,aAAa,CAAC,CAAC;wBACrD,SAAS,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,aAAa,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;oBAC/D,CAAC;gBACH,CAAC;gBAED,sBAAsB;gBACtB,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;oBAAE,OAAO;gBAEjC,MAAM,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;gBAC/B,IAAI,CAAC,SAAS;oBAAE,OAAO;gBAEvB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;oBAC1C,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;oBAC1B,IAAI,CAAC,IAAI;wBAAE,SAAS;oBAEpB,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC;wBACjC,OAAO,CAAC,MAAM,CAAC;4BACb,IAAI,EAAE,IAAI,CAAC,GAAG;4BACd,SAAS,EAAE,uBAAuB;4BAClC,IAAI,EAAE;gCACJ,UAAU,EAAE,SAAS,CAAC,SAAS;gCAC/B,KAAK,EAAE,SAAS,CAAC,IAAI;gCACrB,UAAU,EAAE,IAAI,CAAC,SAAS;gCAC1B,KAAK,EAAE,IAAI,CAAC,IAAI;6BACjB;yBACF,CAAC,CAAC;wBACH,6BAA6B;wBAC7B,MAAM;oBACR,CAAC;gBACH,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: no-duplicate-decorators
|
|
3
|
+
*
|
|
4
|
+
* Ensures a field doesn't have the same decorator applied multiple times.
|
|
5
|
+
*
|
|
6
|
+
* Invalid:
|
|
7
|
+
* @Field({ displayName: "First" })
|
|
8
|
+
* @Field({ displayName: "Second" }) // Duplicate
|
|
9
|
+
* field!: string;
|
|
10
|
+
*
|
|
11
|
+
* @EnumOptions(["a", "b"])
|
|
12
|
+
* @EnumOptions(["x", "y"]) // Duplicate
|
|
13
|
+
* field!: string;
|
|
14
|
+
*/
|
|
15
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
16
|
+
export declare const noDuplicateDecorators: ESLintUtils.RuleModule<"duplicateDecorator", [], unknown, ESLintUtils.RuleListener> & {
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=no-duplicate-decorators.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-duplicate-decorators.d.ts","sourceRoot":"","sources":["../../src/rules/no-duplicate-decorators.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AASvD,eAAO,MAAM,qBAAqB;;CAyChC,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: no-duplicate-decorators
|
|
3
|
+
*
|
|
4
|
+
* Ensures a field doesn't have the same decorator applied multiple times.
|
|
5
|
+
*
|
|
6
|
+
* Invalid:
|
|
7
|
+
* @Field({ displayName: "First" })
|
|
8
|
+
* @Field({ displayName: "Second" }) // Duplicate
|
|
9
|
+
* field!: string;
|
|
10
|
+
*
|
|
11
|
+
* @EnumOptions(["a", "b"])
|
|
12
|
+
* @EnumOptions(["x", "y"]) // Duplicate
|
|
13
|
+
* field!: string;
|
|
14
|
+
*/
|
|
15
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
16
|
+
import { getFormSpecDecorators, FORMSPEC_DECORATORS } from "../utils/decorator-utils.js";
|
|
17
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://formspec.dev/eslint-plugin/rules/${name}`);
|
|
18
|
+
export const noDuplicateDecorators = createRule({
|
|
19
|
+
name: "no-duplicate-decorators",
|
|
20
|
+
meta: {
|
|
21
|
+
type: "problem",
|
|
22
|
+
docs: {
|
|
23
|
+
description: "Ensures a field doesn't have the same decorator applied multiple times",
|
|
24
|
+
},
|
|
25
|
+
messages: {
|
|
26
|
+
duplicateDecorator: "Duplicate @{{decorator}} decorator. Each decorator should only appear once per field.",
|
|
27
|
+
},
|
|
28
|
+
schema: [],
|
|
29
|
+
},
|
|
30
|
+
defaultOptions: [],
|
|
31
|
+
create(context) {
|
|
32
|
+
return {
|
|
33
|
+
PropertyDefinition(node) {
|
|
34
|
+
const decorators = getFormSpecDecorators(node);
|
|
35
|
+
if (decorators.length < 2)
|
|
36
|
+
return;
|
|
37
|
+
// Track seen decorators
|
|
38
|
+
const seen = new Map();
|
|
39
|
+
for (const decorator of decorators) {
|
|
40
|
+
if (!FORMSPEC_DECORATORS.has(decorator.name))
|
|
41
|
+
continue;
|
|
42
|
+
if (seen.has(decorator.name)) {
|
|
43
|
+
context.report({
|
|
44
|
+
node: decorator.node,
|
|
45
|
+
messageId: "duplicateDecorator",
|
|
46
|
+
data: {
|
|
47
|
+
decorator: decorator.name,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
seen.set(decorator.name, true);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
//# sourceMappingURL=no-duplicate-decorators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-duplicate-decorators.js","sourceRoot":"","sources":["../../src/rules/no-duplicate-decorators.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAEzF,MAAM,UAAU,GAAG,WAAW,CAAC,WAAW,CACxC,CAAC,IAAI,EAAE,EAAE,CAAC,4CAA4C,IAAI,EAAE,CAC7D,CAAC;AAIF,MAAM,CAAC,MAAM,qBAAqB,GAAG,UAAU,CAAiB;IAC9D,IAAI,EAAE,yBAAyB;IAC/B,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EAAE,wEAAwE;SACtF;QACD,QAAQ,EAAE;YACR,kBAAkB,EAChB,uFAAuF;SAC1F;QACD,MAAM,EAAE,EAAE;KACX;IACD,cAAc,EAAE,EAAE;IAClB,MAAM,CAAC,OAAO;QACZ,OAAO;YACL,kBAAkB,CAAC,IAAI;gBACrB,MAAM,UAAU,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;gBAC/C,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;oBAAE,OAAO;gBAElC,wBAAwB;gBACxB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAmB,CAAC;gBAExC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;oBACnC,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC;wBAAE,SAAS;oBAEvD,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;wBAC7B,OAAO,CAAC,MAAM,CAAC;4BACb,IAAI,EAAE,SAAS,CAAC,IAAI;4BACpB,SAAS,EAAE,oBAAoB;4BAC/B,IAAI,EAAE;gCACJ,SAAS,EAAE,SAAS,CAAC,IAAI;6BAC1B;yBACF,CAAC,CAAC;oBACL,CAAC;yBAAM,CAAC;wBACN,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;oBACjC,CAAC;gBACH,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: prefer-custom-decorator
|
|
3
|
+
*
|
|
4
|
+
* When a project has symbols with FormSpecExtendsBrand<'Field'> type,
|
|
5
|
+
* flags direct usage of @Field from @formspec/decorators.
|
|
6
|
+
*
|
|
7
|
+
* TODO: Full implementation requires type-checker integration to detect
|
|
8
|
+
* FormSpecExtendsBrand symbols in scope. Currently implemented as a
|
|
9
|
+
* configurable rule that accepts a list of decorator names that should
|
|
10
|
+
* be preferred over @Field.
|
|
11
|
+
*
|
|
12
|
+
* Config example:
|
|
13
|
+
* "@formspec/prefer-custom-decorator": ["warn", { prefer: { "Field": "MyCustomField" } }]
|
|
14
|
+
*/
|
|
15
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
16
|
+
export interface PreferCustomDecoratorOptions {
|
|
17
|
+
prefer: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
export declare const preferCustomDecorator: ESLintUtils.RuleModule<"preferCustomDecorator", [PreferCustomDecoratorOptions], unknown, ESLintUtils.RuleListener> & {
|
|
20
|
+
name: string;
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=prefer-custom-decorator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-custom-decorator.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-custom-decorator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AASvD,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,eAAO,MAAM,qBAAqB;;CAwDhC,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: prefer-custom-decorator
|
|
3
|
+
*
|
|
4
|
+
* When a project has symbols with FormSpecExtendsBrand<'Field'> type,
|
|
5
|
+
* flags direct usage of @Field from @formspec/decorators.
|
|
6
|
+
*
|
|
7
|
+
* TODO: Full implementation requires type-checker integration to detect
|
|
8
|
+
* FormSpecExtendsBrand symbols in scope. Currently implemented as a
|
|
9
|
+
* configurable rule that accepts a list of decorator names that should
|
|
10
|
+
* be preferred over @Field.
|
|
11
|
+
*
|
|
12
|
+
* Config example:
|
|
13
|
+
* "@formspec/prefer-custom-decorator": ["warn", { prefer: { "Field": "MyCustomField" } }]
|
|
14
|
+
*/
|
|
15
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
16
|
+
import { getFormSpecDecorators } from "../utils/decorator-utils.js";
|
|
17
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://formspec.dev/eslint-plugin/rules/${name}`);
|
|
18
|
+
export const preferCustomDecorator = createRule({
|
|
19
|
+
name: "prefer-custom-decorator",
|
|
20
|
+
meta: {
|
|
21
|
+
type: "suggestion",
|
|
22
|
+
docs: {
|
|
23
|
+
description: "Suggests using a custom decorator instead of a built-in FormSpec decorator when a project-specific alternative exists",
|
|
24
|
+
},
|
|
25
|
+
messages: {
|
|
26
|
+
preferCustomDecorator: "Prefer @{{preferred}} over @{{builtin}}. This project provides a custom decorator that extends the built-in.",
|
|
27
|
+
},
|
|
28
|
+
schema: [
|
|
29
|
+
{
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
prefer: {
|
|
33
|
+
type: "object",
|
|
34
|
+
additionalProperties: { type: "string" },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ["prefer"],
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
defaultOptions: [{ prefer: {} }],
|
|
43
|
+
create(context, [options]) {
|
|
44
|
+
const preferMap = options.prefer;
|
|
45
|
+
const builtinNames = new Set(Object.keys(preferMap));
|
|
46
|
+
if (builtinNames.size === 0)
|
|
47
|
+
return {};
|
|
48
|
+
return {
|
|
49
|
+
PropertyDefinition(node) {
|
|
50
|
+
const decorators = getFormSpecDecorators(node);
|
|
51
|
+
if (decorators.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
for (const decorator of decorators) {
|
|
54
|
+
if (builtinNames.has(decorator.name)) {
|
|
55
|
+
const preferred = preferMap[decorator.name];
|
|
56
|
+
if (!preferred)
|
|
57
|
+
continue;
|
|
58
|
+
context.report({
|
|
59
|
+
node: decorator.node,
|
|
60
|
+
messageId: "preferCustomDecorator",
|
|
61
|
+
data: {
|
|
62
|
+
builtin: decorator.name,
|
|
63
|
+
preferred,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
//# sourceMappingURL=prefer-custom-decorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-custom-decorator.js","sourceRoot":"","sources":["../../src/rules/prefer-custom-decorator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAEpE,MAAM,UAAU,GAAG,WAAW,CAAC,WAAW,CACxC,CAAC,IAAI,EAAE,EAAE,CAAC,4CAA4C,IAAI,EAAE,CAC7D,CAAC;AAQF,MAAM,CAAC,MAAM,qBAAqB,GAAG,UAAU,CAA6C;IAC1F,IAAI,EAAE,yBAAyB;IAC/B,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;YACJ,WAAW,EACT,uHAAuH;SAC1H;QACD,QAAQ,EAAE;YACR,qBAAqB,EACnB,8GAA8G;SACjH;QACD,MAAM,EAAE;YACN;gBACE,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,MAAM,EAAE;wBACN,IAAI,EAAE,QAAQ;wBACd,oBAAoB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;qBACzC;iBACF;gBACD,QAAQ,EAAE,CAAC,QAAQ,CAAC;gBACpB,oBAAoB,EAAE,KAAK;aAC5B;SACF;KACF;IACD,cAAc,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IAChC,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC;QACvB,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;QACjC,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;QAErD,IAAI,YAAY,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAEvC,OAAO;YACL,kBAAkB,CAAC,IAAI;gBACrB,MAAM,UAAU,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;gBAC/C,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;oBAAE,OAAO;gBAEpC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;oBACnC,IAAI,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;wBACrC,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;wBAC5C,IAAI,CAAC,SAAS;4BAAE,SAAS;wBAEzB,OAAO,CAAC,MAAM,CAAC;4BACb,IAAI,EAAE,SAAS,CAAC,IAAI;4BACpB,SAAS,EAAE,uBAAuB;4BAClC,IAAI,EAAE;gCACJ,OAAO,EAAE,SAAS,CAAC,IAAI;gCACvB,SAAS;6BACV;yBACF,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: showwhen-field-exists
|
|
3
|
+
*
|
|
4
|
+
* Ensures @ShowWhen references a field that exists in the same class.
|
|
5
|
+
*
|
|
6
|
+
* Valid:
|
|
7
|
+
* @EnumOptions(["a", "b"])
|
|
8
|
+
* type!: "a" | "b";
|
|
9
|
+
*
|
|
10
|
+
* @ShowWhen({ _predicate: "equals", field: "type", value: "a" })
|
|
11
|
+
* conditionalField?: string;
|
|
12
|
+
*
|
|
13
|
+
* Invalid:
|
|
14
|
+
* @ShowWhen({ _predicate: "equals", field: "nonexistent", value: "x" })
|
|
15
|
+
* field?: string; // "nonexistent" doesn't exist
|
|
16
|
+
*/
|
|
17
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
18
|
+
export declare const showwhenFieldExists: ESLintUtils.RuleModule<"fieldDoesNotExist", [], unknown, ESLintUtils.RuleListener> & {
|
|
19
|
+
name: string;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=showwhen-field-exists.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"showwhen-field-exists.d.ts","sourceRoot":"","sources":["../../src/rules/showwhen-field-exists.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,WAAW,EAAkB,MAAM,0BAA0B,CAAC;AAavE,eAAO,MAAM,mBAAmB;;CAoD9B,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: showwhen-field-exists
|
|
3
|
+
*
|
|
4
|
+
* Ensures @ShowWhen references a field that exists in the same class.
|
|
5
|
+
*
|
|
6
|
+
* Valid:
|
|
7
|
+
* @EnumOptions(["a", "b"])
|
|
8
|
+
* type!: "a" | "b";
|
|
9
|
+
*
|
|
10
|
+
* @ShowWhen({ _predicate: "equals", field: "type", value: "a" })
|
|
11
|
+
* conditionalField?: string;
|
|
12
|
+
*
|
|
13
|
+
* Invalid:
|
|
14
|
+
* @ShowWhen({ _predicate: "equals", field: "nonexistent", value: "x" })
|
|
15
|
+
* field?: string; // "nonexistent" doesn't exist
|
|
16
|
+
*/
|
|
17
|
+
import { ESLintUtils, AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
18
|
+
import { findDecorator, getShowWhenField, getClassPropertyNames, } from "../utils/decorator-utils.js";
|
|
19
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://formspec.dev/eslint-plugin/rules/${name}`);
|
|
20
|
+
export const showwhenFieldExists = createRule({
|
|
21
|
+
name: "showwhen-field-exists",
|
|
22
|
+
meta: {
|
|
23
|
+
type: "problem",
|
|
24
|
+
docs: {
|
|
25
|
+
description: "Ensures @ShowWhen references a field that exists in the same class",
|
|
26
|
+
},
|
|
27
|
+
messages: {
|
|
28
|
+
fieldDoesNotExist: "@ShowWhen references field '{{referencedField}}' which does not exist in this class",
|
|
29
|
+
},
|
|
30
|
+
schema: [],
|
|
31
|
+
},
|
|
32
|
+
defaultOptions: [],
|
|
33
|
+
create(context) {
|
|
34
|
+
return {
|
|
35
|
+
PropertyDefinition(node) {
|
|
36
|
+
const showWhenDecorator = findDecorator(node, "ShowWhen");
|
|
37
|
+
if (!showWhenDecorator)
|
|
38
|
+
return;
|
|
39
|
+
const referencedField = getShowWhenField(showWhenDecorator);
|
|
40
|
+
if (!referencedField)
|
|
41
|
+
return;
|
|
42
|
+
// Find the parent class
|
|
43
|
+
// PropertyDefinition -> ClassBody -> ClassDeclaration/ClassExpression
|
|
44
|
+
// TypeScript guarantees node.parent is ClassBody for PropertyDefinition
|
|
45
|
+
const classBody = node.parent;
|
|
46
|
+
const classNode = classBody.parent;
|
|
47
|
+
if (classNode.type !== AST_NODE_TYPES.ClassDeclaration &&
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive check
|
|
49
|
+
classNode.type !== AST_NODE_TYPES.ClassExpression) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Get all property names in the class
|
|
53
|
+
const propertyNames = getClassPropertyNames(classNode);
|
|
54
|
+
// Check if the referenced field exists
|
|
55
|
+
if (!propertyNames.has(referencedField)) {
|
|
56
|
+
context.report({
|
|
57
|
+
node: showWhenDecorator.node,
|
|
58
|
+
messageId: "fieldDoesNotExist",
|
|
59
|
+
data: {
|
|
60
|
+
referencedField,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
//# sourceMappingURL=showwhen-field-exists.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"showwhen-field-exists.js","sourceRoot":"","sources":["../../src/rules/showwhen-field-exists.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AACvE,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,6BAA6B,CAAC;AAErC,MAAM,UAAU,GAAG,WAAW,CAAC,WAAW,CACxC,CAAC,IAAI,EAAE,EAAE,CAAC,4CAA4C,IAAI,EAAE,CAC7D,CAAC;AAIF,MAAM,CAAC,MAAM,mBAAmB,GAAG,UAAU,CAAiB;IAC5D,IAAI,EAAE,uBAAuB;IAC7B,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EAAE,oEAAoE;SAClF;QACD,QAAQ,EAAE;YACR,iBAAiB,EACf,qFAAqF;SACxF;QACD,MAAM,EAAE,EAAE;KACX;IACD,cAAc,EAAE,EAAE;IAClB,MAAM,CAAC,OAAO;QACZ,OAAO;YACL,kBAAkB,CAAC,IAAI;gBACrB,MAAM,iBAAiB,GAAG,aAAa,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;gBAC1D,IAAI,CAAC,iBAAiB;oBAAE,OAAO;gBAE/B,MAAM,eAAe,GAAG,gBAAgB,CAAC,iBAAiB,CAAC,CAAC;gBAC5D,IAAI,CAAC,eAAe;oBAAE,OAAO;gBAE7B,wBAAwB;gBACxB,sEAAsE;gBACtE,wEAAwE;gBACxE,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC;gBAC9B,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,CAAC;gBACnC,IACE,SAAS,CAAC,IAAI,KAAK,cAAc,CAAC,gBAAgB;oBAClD,0FAA0F;oBAC1F,SAAS,CAAC,IAAI,KAAK,cAAc,CAAC,eAAe,EACjD,CAAC;oBACD,OAAO;gBACT,CAAC;gBAED,sCAAsC;gBACtC,MAAM,aAAa,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC;gBAEvD,uCAAuC;gBACvC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,CAAC;oBACxC,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI,EAAE,iBAAiB,CAAC,IAAI;wBAC5B,SAAS,EAAE,mBAAmB;wBAC9B,IAAI,EAAE;4BACJ,eAAe;yBAChB;qBACF,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: showwhen-suggests-optional
|
|
3
|
+
*
|
|
4
|
+
* Suggests that fields with @ShowWhen should be marked as optional (`?`)
|
|
5
|
+
* since they may not be present in the output when the condition is false.
|
|
6
|
+
*
|
|
7
|
+
* Valid:
|
|
8
|
+
* @ShowWhen({ _predicate: "equals", field: "type", value: "a" })
|
|
9
|
+
* conditionalField?: string; // Optional - good
|
|
10
|
+
*
|
|
11
|
+
* Warning:
|
|
12
|
+
* @ShowWhen({ _predicate: "equals", field: "type", value: "a" })
|
|
13
|
+
* conditionalField!: string; // Not optional - may cause issues
|
|
14
|
+
*/
|
|
15
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
16
|
+
export declare const showwhenSuggestsOptional: ESLintUtils.RuleModule<"shouldBeOptional", [], unknown, ESLintUtils.RuleListener> & {
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=showwhen-suggests-optional.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"showwhen-suggests-optional.d.ts","sourceRoot":"","sources":["../../src/rules/showwhen-suggests-optional.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,WAAW,EAAkB,MAAM,0BAA0B,CAAC;AAUvE,eAAO,MAAM,wBAAwB;;CAoCnC,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: showwhen-suggests-optional
|
|
3
|
+
*
|
|
4
|
+
* Suggests that fields with @ShowWhen should be marked as optional (`?`)
|
|
5
|
+
* since they may not be present in the output when the condition is false.
|
|
6
|
+
*
|
|
7
|
+
* Valid:
|
|
8
|
+
* @ShowWhen({ _predicate: "equals", field: "type", value: "a" })
|
|
9
|
+
* conditionalField?: string; // Optional - good
|
|
10
|
+
*
|
|
11
|
+
* Warning:
|
|
12
|
+
* @ShowWhen({ _predicate: "equals", field: "type", value: "a" })
|
|
13
|
+
* conditionalField!: string; // Not optional - may cause issues
|
|
14
|
+
*/
|
|
15
|
+
import { ESLintUtils, AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
16
|
+
import { findDecorator } from "../utils/decorator-utils.js";
|
|
17
|
+
import { isOptionalProperty } from "../utils/type-utils.js";
|
|
18
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://formspec.dev/eslint-plugin/rules/${name}`);
|
|
19
|
+
export const showwhenSuggestsOptional = createRule({
|
|
20
|
+
name: "showwhen-suggests-optional",
|
|
21
|
+
meta: {
|
|
22
|
+
type: "suggestion",
|
|
23
|
+
docs: {
|
|
24
|
+
description: "Suggests that fields with @ShowWhen should be marked as optional",
|
|
25
|
+
},
|
|
26
|
+
messages: {
|
|
27
|
+
shouldBeOptional: "Field '{{field}}' uses @ShowWhen but is not optional. Consider adding '?' since the field may not be in the output when the condition is false.",
|
|
28
|
+
},
|
|
29
|
+
schema: [],
|
|
30
|
+
},
|
|
31
|
+
defaultOptions: [],
|
|
32
|
+
create(context) {
|
|
33
|
+
return {
|
|
34
|
+
PropertyDefinition(node) {
|
|
35
|
+
const showWhenDecorator = findDecorator(node, "ShowWhen");
|
|
36
|
+
if (!showWhenDecorator)
|
|
37
|
+
return;
|
|
38
|
+
// Check if the field is already optional
|
|
39
|
+
if (isOptionalProperty(node))
|
|
40
|
+
return;
|
|
41
|
+
const fieldName = node.key.type === AST_NODE_TYPES.Identifier ? node.key.name : "<computed>";
|
|
42
|
+
context.report({
|
|
43
|
+
node: node.key,
|
|
44
|
+
messageId: "shouldBeOptional",
|
|
45
|
+
data: {
|
|
46
|
+
field: fieldName,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
//# sourceMappingURL=showwhen-suggests-optional.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"showwhen-suggests-optional.js","sourceRoot":"","sources":["../../src/rules/showwhen-suggests-optional.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE5D,MAAM,UAAU,GAAG,WAAW,CAAC,WAAW,CACxC,CAAC,IAAI,EAAE,EAAE,CAAC,4CAA4C,IAAI,EAAE,CAC7D,CAAC;AAIF,MAAM,CAAC,MAAM,wBAAwB,GAAG,UAAU,CAAiB;IACjE,IAAI,EAAE,4BAA4B;IAClC,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;YACJ,WAAW,EAAE,kEAAkE;SAChF;QACD,QAAQ,EAAE;YACR,gBAAgB,EACd,iJAAiJ;SACpJ;QACD,MAAM,EAAE,EAAE;KACX;IACD,cAAc,EAAE,EAAE;IAClB,MAAM,CAAC,OAAO;QACZ,OAAO;YACL,kBAAkB,CAAC,IAAI;gBACrB,MAAM,iBAAiB,GAAG,aAAa,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;gBAC1D,IAAI,CAAC,iBAAiB;oBAAE,OAAO;gBAE/B,yCAAyC;gBACzC,IAAI,kBAAkB,CAAC,IAAI,CAAC;oBAAE,OAAO;gBAErC,MAAM,SAAS,GACb,IAAI,CAAC,GAAG,CAAC,IAAI,KAAK,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC;gBAE7E,OAAO,CAAC,MAAM,CAAC;oBACb,IAAI,EAAE,IAAI,CAAC,GAAG;oBACd,SAAS,EAAE,kBAAkB;oBAC7B,IAAI,EAAE;wBACJ,KAAK,EAAE,SAAS;qBACjB;iBACF,CAAC,CAAC;YACL,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|