Skip to content

Commit f7bb071

Browse files
authored
Add a model validation that detects duplicate properties on ancestor classes (#507)
1 parent 00d9414 commit f7bb071

File tree

1 file changed

+64
-6
lines changed

1 file changed

+64
-6
lines changed

compiler/steps/validate-model.ts

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -384,12 +384,14 @@ export default async function validateModel (apiModel: model.Model, restSpec: Ma
384384
// valid overlaps, with some parameters that can also be represented as body properties.
385385
// Client generators will have to take care of this.
386386

387+
const inheritedProps = inheritedProperties(typeDef)
388+
387389
context.push('path')
388-
validateProperties(typeDef.path, openGenerics)
390+
validateProperties(typeDef.path, openGenerics, inheritedProps)
389391
context.pop()
390392

391393
context.push('query')
392-
validateProperties(typeDef.query, openGenerics)
394+
validateProperties(typeDef.query, openGenerics, inheritedProps)
393395
context.pop()
394396

395397
context.push('body')
@@ -400,7 +402,7 @@ export default async function validateModel (apiModel: model.Model, restSpec: Ma
400402
}
401403
switch (typeDef.body.kind) {
402404
case 'properties':
403-
validateProperties(typeDef.body.properties, openGenerics)
405+
validateProperties(typeDef.body.properties, openGenerics, inheritedProps)
404406
break
405407
case 'value':
406408
validateValueOf(typeDef.body.value, openGenerics)
@@ -435,7 +437,7 @@ export default async function validateModel (apiModel: model.Model, restSpec: Ma
435437
}
436438
switch (typeDef.body.kind) {
437439
case 'properties':
438-
validateProperties(typeDef.body.properties, openGenerics)
440+
validateProperties(typeDef.body.properties, openGenerics, inheritedProperties(typeDef))
439441
break
440442
case 'value':
441443
validateValueOf(typeDef.body.value, openGenerics)
@@ -451,13 +453,65 @@ export default async function validateModel (apiModel: model.Model, restSpec: Ma
451453
context.pop()
452454
}
453455

456+
/** Collects the name of inherited properties. Names are normalized to lower case (see also validateProperties) */
457+
function inheritedProperties (typeDef: model.Interface | model.Request | model.Response, accum?: Set<string>): Set<string> {
458+
const result = accum ?? new Set<string>()
459+
460+
function addProperties (props: model.Property[]): void {
461+
props.forEach(prop => result.add((prop.identifier ?? prop.name).toLowerCase()))
462+
}
463+
464+
function addInherits (inherits?: model.Inherits): void {
465+
if (inherits == null) {
466+
return
467+
}
468+
469+
const typeDef = getTypeDef(inherits.type)
470+
if (typeDef == null) {
471+
modelError(`No type definition for parent '${fqn(inherits.type)}'`)
472+
return
473+
}
474+
475+
switch (typeDef?.kind) {
476+
case 'request':
477+
inheritedProperties(typeDef, result)
478+
addProperties(typeDef.path)
479+
addProperties(typeDef.query)
480+
if (typeDef.body.kind === 'properties') {
481+
addProperties(typeDef.body.properties)
482+
}
483+
break
484+
485+
case 'response':
486+
inheritedProperties(typeDef, result)
487+
if (typeDef.body.kind === 'properties') {
488+
addProperties(typeDef.body.properties)
489+
}
490+
break
491+
492+
case 'interface':
493+
inheritedProperties(typeDef, result)
494+
addProperties(typeDef.properties)
495+
break
496+
}
497+
}
498+
499+
if (typeDef.inherits != null) {
500+
addInherits(typeDef.inherits)
501+
}
502+
typeDef.implements?.forEach(addInherits)
503+
typeDef.behaviors?.forEach(addInherits)
504+
505+
return result
506+
}
507+
454508
function validateInterface (typeDef: model.Interface): void {
455509
const openGenerics = openGenericSet(typeDef)
456510

457511
validateImplements(typeDef.implements, openGenerics)
458512
validateInherits(typeDef.inherits, openGenerics)
459513
validateBehaviors(typeDef, openGenerics)
460-
validateProperties(typeDef.properties, openGenerics)
514+
validateProperties(typeDef.properties, openGenerics, inheritedProperties(typeDef))
461515

462516
if (typeDef.variants?.kind === 'container') {
463517
const variants = typeDef.properties.filter(prop => !(prop.containerProperty ?? false))
@@ -639,7 +693,7 @@ export default async function validateModel (apiModel: model.Model, restSpec: Ma
639693
return false
640694
}
641695

642-
function validateProperties (props: model.Property[], openGenerics: Set<string>): void {
696+
function validateProperties (props: model.Property[], openGenerics: Set<string>, inheritedProperties: Set<string>): void {
643697
const allIdentifiers = new Set<string>()
644698
const allNames = new Set<string>()
645699

@@ -651,6 +705,10 @@ export default async function validateModel (apiModel: model.Model, restSpec: Ma
651705
}
652706
allIdentifiers.add(identifier)
653707

708+
if (inheritedProperties.has(identifier)) {
709+
modelError(`Property '${prop.name}' is already defined in an ancestor class`)
710+
}
711+
654712
// Names and aliases must be unique among all items (case sensitive)
655713
const names = [prop.name].concat(prop.aliases ?? [])
656714
for (const name of names) {

0 commit comments

Comments
 (0)