Skip to content

Commit 5b0d747

Browse files
Harden enum-value-formatting meta:enum handling (#821)
# Harden enum-value-formatting check Fixes #820 ## Summary Harden `meta:enum` handling to be deterministic and safe: use own-property checks, require `meta:enum` to be a plain object (emit explicit error when invalid), fix coverage to check key presence (not truthiness), improve `meta:enum` paths, and de-duplicate enum value issues. No API/config changes. ## Changes - Detect `meta:enum` via `Object.prototype.hasOwnProperty.call(parent, 'meta:enum')` (no prototype-chain lookup). - Validate `meta:enum` is a plain object; emit one ERROR when invalid type. - Coverage: use own-key presence (`hasOwnProperty`) on `meta:enum` (no truthiness false-positives, no inherited keys). - Report “missing description” at the `meta:enum` path; fix extra-keys path derivation. - Iterate over unique enum string values to avoid duplicate issues.
2 parents 5127956 + 5162baf commit 5b0d747

File tree

1 file changed

+47
-18
lines changed

1 file changed

+47
-18
lines changed

tools/src/main/js/linter/checks/enum-value-formatting.check.js

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,47 @@ class EnumValueFormattingCheck extends LintCheck {
6464
// Skip empty enums
6565
if (node.length === 0) return;
6666

67-
// Check if meta:enum exists on parent
68-
const hasMetaEnum = parent && 'meta:enum' in parent;
69-
const metaEnum = hasMetaEnum ? parent['meta:enum'] : {};
70-
67+
const loc = (p) => (typeof p === 'string' && p.length ? p : '(unknown path)');
68+
// meta:enum is sibling of enum; path is from traverseSchema (contract: path for this node).
69+
const metaEnumBasePath = !path
70+
? 'meta:enum'
71+
: path.endsWith('.enum')
72+
? path.replace(/\.enum$/, '.meta:enum')
73+
: path === 'enum'
74+
? 'meta:enum'
75+
: `${path}.meta:enum`;
76+
77+
// Check if meta:enum exists on parent (own property only; avoid prototype chain)
78+
const hasMetaEnum =
79+
!!parent && Object.prototype.hasOwnProperty.call(parent, 'meta:enum');
80+
const metaEnumRaw = hasMetaEnum ? parent['meta:enum'] : null;
81+
const metaEnumIsPlainObject =
82+
metaEnumRaw !== null &&
83+
typeof metaEnumRaw === 'object' &&
84+
!Array.isArray(metaEnumRaw) &&
85+
(Object.getPrototypeOf(metaEnumRaw) === Object.prototype ||
86+
Object.getPrototypeOf(metaEnumRaw) === null);
87+
88+
if (hasMetaEnum && !metaEnumIsPlainObject) {
89+
issues.push(this.createIssue(
90+
'meta:enum must be a plain object mapping enum values to descriptions.',
91+
loc(metaEnumBasePath),
92+
{
93+
actualType: Array.isArray(metaEnumRaw)
94+
? 'array'
95+
: typeof metaEnumRaw
96+
},
97+
Severity.ERROR
98+
));
99+
}
100+
101+
const metaEnum = metaEnumIsPlainObject ? metaEnumRaw : {};
102+
const enumSet = new Set(node.filter(v => typeof v === 'string'));
103+
71104
// Track detected case styles
72105
const detectedStyles = new Map();
73106

74-
for (const value of node) {
75-
// Skip non-string values
76-
if (typeof value !== 'string') continue;
77-
107+
for (const value of enumSet) {
78108
// Detect case style
79109
const style = this.detectCaseStyle(value);
80110

@@ -87,7 +117,7 @@ class EnumValueFormattingCheck extends LintCheck {
87117
if (/\s/.test(value)) {
88118
issues.push(this.createIssue(
89119
`Enum value "${value}" contains whitespace. Use ${preferredCase} instead.`,
90-
`${path}`,
120+
loc(path),
91121
{
92122
value,
93123
suggestion: this.convertToCase(value, preferredCase)
@@ -100,16 +130,16 @@ class EnumValueFormattingCheck extends LintCheck {
100130
if (/[^a-zA-Z0-9_-]/.test(value)) {
101131
issues.push(this.createIssue(
102132
`Enum value "${value}" contains special characters.`,
103-
`${path}`,
133+
loc(path),
104134
{ value }
105135
));
106136
}
107137

108-
// Check meta:enum coverage only if meta:enum exists
109-
if (hasMetaEnum && !metaEnum[value]) {
138+
// Check meta:enum coverage only when meta:enum is a plain object
139+
if (hasMetaEnum && metaEnumIsPlainObject && !Object.prototype.hasOwnProperty.call(metaEnum, value)) {
110140
issues.push(this.createIssue(
111141
`Enum value "${value}" is missing a description in meta:enum.`,
112-
`${path}`,
142+
loc(metaEnumBasePath),
113143
{ value },
114144
Severity.ERROR
115145
));
@@ -125,22 +155,21 @@ class EnumValueFormattingCheck extends LintCheck {
125155
issues.push(this.createIssue(
126156
`Enum values use inconsistent case styles: ${stylesUsed}. ` +
127157
`Consider using ${preferredCase} consistently.`,
128-
path,
158+
loc(path),
129159
{
130160
detectedStyles: Object.fromEntries(detectedStyles)
131161
},
132162
Severity.INFO
133163
));
134164
}
135165

136-
// Check if meta:enum has extra values not in enum
137-
if (hasMetaEnum) {
138-
const enumSet = new Set(node.filter(v => typeof v === 'string'));
166+
// Check if meta:enum has extra values not in enum (only when it's a plain object)
167+
if (hasMetaEnum && metaEnumIsPlainObject) {
139168
for (const metaKey of Object.keys(metaEnum)) {
140169
if (!enumSet.has(metaKey)) {
141170
issues.push(this.createIssue(
142171
`meta:enum contains "${metaKey}" which is not in the enum array.`,
143-
`${path.replace('.enum', '.meta:enum')}.${metaKey}`,
172+
`${metaEnumBasePath}.${metaKey}`,
144173
{ value: metaKey }
145174
));
146175
}

0 commit comments

Comments
 (0)