Skip to content

Commit f53f0b3

Browse files
web-padawanclaude
andauthored
chore: resolve missing inherited members in CEM class declarations (#11381)
The CEM analyzer only follows inheritedFrom one level deep, so members inherited through 2+ levels (e.g., EmailField → TextFieldMixin → FieldMixin → LabelMixin) are dropped from class declarations. Add a multi-pass resolution step in packageLinkPhase that copies missing members and attributes from mixin/superclass declarations to class declarations until the inheritance chain is fully resolved. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e8e05fe commit f53f0b3

File tree

1 file changed

+59
-0
lines changed

1 file changed

+59
-0
lines changed

custom-elements-manifest.config.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,53 @@ function classPrivacyPlugin() {
217217
};
218218
}
219219

220+
function copyMissingItems(target, source, key) {
221+
const existing = new Set((target[key] || []).map((item) => item.name));
222+
let changed = false;
223+
for (const item of source[key] || []) {
224+
if (!existing.has(item.name)) {
225+
target[key] ||= [];
226+
target[key].push({ ...item });
227+
existing.add(item.name);
228+
changed = true;
229+
}
230+
}
231+
return changed;
232+
}
233+
234+
function copyMissingFromParents(decl, allDeclarations) {
235+
const parentRefs = [...(decl.mixins || [])];
236+
if (decl.superclass?.name) {
237+
parentRefs.push(decl.superclass);
238+
}
239+
240+
let changed = false;
241+
for (const parentRef of parentRefs) {
242+
const parentDecl = allDeclarations.get(parentRef.name);
243+
if (!parentDecl) continue;
244+
changed = copyMissingItems(decl, parentDecl, 'members') || changed;
245+
changed = copyMissingItems(decl, parentDecl, 'attributes') || changed;
246+
}
247+
return changed;
248+
}
249+
250+
/**
251+
* Copy missing members and attributes from mixin/superclass declarations
252+
* to class declarations. Runs multiple passes to handle multi-level
253+
* inheritance chains (e.g., EmailField → TextField → FieldMixin → LabelMixin).
254+
*/
255+
function resolveInheritedMembers(allDeclarations) {
256+
const classDeclarations = [...allDeclarations.values()].filter((d) => d.kind === 'class');
257+
258+
let changed = true;
259+
while (changed) {
260+
changed = false;
261+
for (const decl of classDeclarations) {
262+
changed = copyMissingFromParents(decl, allDeclarations) || changed;
263+
}
264+
}
265+
}
266+
220267
export default {
221268
globs: ['packages/**/src/(vaadin-*.js|*-mixin.js)'],
222269
packagejson: false,
@@ -227,6 +274,18 @@ export default {
227274
readonlyPlugin(),
228275
{
229276
packageLinkPhase({ customElementsManifest }) {
277+
const allDeclarations = new Map();
278+
for (const mod of customElementsManifest.modules) {
279+
for (const decl of mod.declarations || []) {
280+
allDeclarations.set(decl.name, decl);
281+
}
282+
}
283+
284+
// Resolve missing inherited members from mixins and superclasses.
285+
// The analyzer only follows inheritedFrom one level deep, so members
286+
// inherited through 2+ levels are dropped from class declarations.
287+
resolveInheritedMembers(allDeclarations);
288+
230289
for (const definition of customElementsManifest.modules) {
231290
// Filter out class declarations marked as @private or @protected
232291
const privateClassNames = new Set(

0 commit comments

Comments
 (0)