Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce primitive to filter model properties and get effective type #506

Merged
merged 13 commits into from
Jun 3, 2022
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cadl-lang/compiler",
"comment": "Add compiler API to filter model properties and get try to find equivalent named models for anonymous models",
"type": "minor"
}
],
"packageName": "@cadl-lang/compiler"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cadl-lang/openapi3",
"comment": "Find good names where possible for anonymous models that differ from named models only by properties that are not part of the schema",
"type": "minor"
}
],
"packageName": "@cadl-lang/openapi3"
}
149 changes: 147 additions & 2 deletions packages/compiler/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ export interface Checker {
value: string | number | boolean,
node?: StringLiteralNode | NumericLiteralNode | BooleanLiteralNode
): StringLiteralType | NumericLiteralType | BooleanLiteralType;
getEffectiveModelType(
model: ModelType,
filter?: (property: ModelTypeProperty) => boolean
): ModelType;
filterModelProperties(
model: ModelType,
filter: (property: ModelTypeProperty) => boolean
): ModelType;

errorType: ErrorType;
voidType: VoidType;
Expand Down Expand Up @@ -302,6 +310,8 @@ export function createChecker(program: Program): Checker {
createFunctionType,
createLiteralType,
finishType,
getEffectiveModelType,
filterModelProperties,
};

const projectionMembers = createProjectionMembers(checker);
Expand Down Expand Up @@ -886,7 +896,10 @@ export function createChecker(program: Program): Checker {
continue;
}

const newPropType = cloneType(prop, { sourceProperty: prop, model: intersection });
const newPropType = cloneType(prop, {
nguerrera marked this conversation as resolved.
Show resolved Hide resolved
sourceProperty: prop,
model: intersection,
});
properties.set(prop.name, newPropType);
}
}
Expand Down Expand Up @@ -1718,7 +1731,10 @@ export function createChecker(program: Program): Checker {

// copy each property
for (const prop of walkPropertiesInherited(targetType)) {
const newProp = cloneType(prop, { sourceProperty: prop, model: parentModel });
const newProp = cloneType(prop, {
sourceProperty: prop,
model: parentModel,
});
props.push(newProp);
}
}
Expand All @@ -1735,6 +1751,19 @@ export function createChecker(program: Program): Checker {
}
}

function countPropertiesInherited(
model: ModelType,
filter?: (property: ModelTypeProperty) => boolean
) {
let count = 0;
for (const each of walkPropertiesInherited(model)) {
if (!filter || filter(each)) {
count++;
}
}
return count;
}

function checkModelProperty(prop: ModelPropertyNode, parentModel?: ModelType): ModelTypeProperty {
const decorators = checkDecorators(prop);
const valueType = getTypeForNode(prop.value);
Expand Down Expand Up @@ -3169,6 +3198,105 @@ export function createChecker(program: Program): Checker {

return parts.reverse().join(".");
}

function getEffectiveModelType(
model: ModelType,
filter?: (property: ModelTypeProperty) => boolean
): ModelType {
if (filter) {
model = filterModelProperties(model, filter);
}
while (true) {
if (model.name) {
// named model
return model;
}

// We would need to change the algorithm if this doesn't hold. We
// assume model has no inherited properties below.
compilerAssert(!model.baseModel, "Anonymous model with base model.");

if (model.properties.size === 0) {
// empty model
return model;
}

let source: ModelType | undefined;

for (const property of model.properties.values()) {
const propertySource = getRootSourceModel(property);
if (!propertySource) {
// unsourced property
return model;
}

if (!source) {
// initialize common source from first sourced property.
source = propertySource;
continue;
}

if (isDerivedFrom(source, propertySource)) {
// OK
} else if (isDerivedFrom(propertySource, source)) {
// OK, but refine common source to derived type.
source = propertySource;
} else {
// different source
return model;
}
}

compilerAssert(source, "Should have found a common source to reach here.");

if (model.properties.size !== countPropertiesInherited(source, filter)) {
// source has additional properties.
return model;
}

// keep going until we reach a model that cannot be further reduced.
model = source;
}
}

function filterModelProperties(
model: ModelType,
filter: (property: ModelTypeProperty) => boolean
): ModelType {
let filtered = false;
for (const property of walkPropertiesInherited(model)) {
if (!filter(property)) {
filtered = true;
break;
}
}

if (!filtered) {
return model;
}

const properties = new Map<string, ModelTypeProperty>();
const newModel: ModelType = createType({
kind: "Model",
node: undefined,
name: "",
properties,
decorators: [],
derivedModels: [],
});

for (const property of walkPropertiesInherited(model)) {
if (filter(property)) {
const newProperty = cloneType(property, {
sourceProperty: property,
model: newModel,
});
properties.set(property.name, newProperty);
}
}

return finishType(newModel);
}
}

function isErrorType(type: Type): type is ErrorType {
Expand All @@ -3178,3 +3306,20 @@ function isErrorType(type: Type): type is ErrorType {
function createUsingSymbol(symbolSource: Sym): Sym {
return { flags: SymbolFlags.Using, declarations: [], name: symbolSource.name, symbolSource };
}

function isDerivedFrom(derived: ModelType, base: ModelType) {
while (derived !== base && derived.baseModel) {
derived = derived.baseModel;
}
return derived === base;
}

function getRootSourceModel(property: ModelTypeProperty): ModelType | undefined {
if (!property.sourceProperty) {
return undefined;
}
while (property.sourceProperty) {
property = property.sourceProperty;
}
return property?.model;
}
4 changes: 2 additions & 2 deletions packages/compiler/core/projector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,8 @@ export function createProjector(
*/
function shouldFinishType(type: ModelType | InterfaceType | UnionType) {
if (
type.node.kind !== SyntaxKind.ModelStatement &&
type.node.kind !== SyntaxKind.InterfaceStatement
type.node?.kind !== SyntaxKind.ModelStatement &&
type.node?.kind !== SyntaxKind.InterfaceStatement
) {
return true;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/core/semantic-walker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ function navigateType(
*/
export function isTemplate(model: ModelType): boolean {
return (
model.node.kind === SyntaxKind.ModelStatement &&
model.node?.kind === SyntaxKind.ModelStatement &&
model.node.templateParameters.length > 0 &&
!model.templateArguments?.length
);
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type DecoratorArgumentValue = Type | number | string | boolean;

export interface DecoratorArgument {
value: DecoratorArgumentValue;
node: Node;
node?: Node;
}

export interface DecoratorApplication {
Expand Down Expand Up @@ -149,7 +149,7 @@ export type IntrinsicModel<T extends IntrinsicModelName = IntrinsicModelName> =
export interface ModelType extends BaseType, DecoratedType, TemplatedType {
kind: "Model";
name: IntrinsicModelName | string;
node:
node?:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when do you not have a node?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I create a new type in filterModelProperties. I wasn't sure about this. I could maybe copy the node from the source model, but I recently found issues with places where we do that elsewhere. See #462. This will be a good thing to cover in design discussion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right I guess that make sense. I think then we might want to think of a sourceMap system so you can figure out where a type like that was introduced from

| ModelStatementNode
| ModelExpressionNode
| IntersectionExpressionNode
Expand Down
Loading