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

Model assembler claims conflicting members - false positive #2004

Closed
kubukoz opened this issue Oct 9, 2023 · 2 comments · Fixed by #2030
Closed

Model assembler claims conflicting members - false positive #2004

kubukoz opened this issue Oct 9, 2023 · 2 comments · Fixed by #2030
Labels
bug This issue is a bug.

Comments

@kubukoz
Copy link
Contributor

kubukoz commented Oct 9, 2023

Consider the following model IDL:

$version: "2"
namespace test

@mixin
structure Common {
  @required description: String
}

structure Str with [Common] {
  description: String = "test"
}

When you load and serialize this to Smithy again, you'll get an apply node for the @default trait:

$version: "2.0"

namespace test

@mixin
structure Common {
    @required
    description: String
}

structure Str with [Common] {}

apply Str$description @default("test")

This is also reflected in the normal ModelSerializer (outputting JSON):

JSON dump
{
    "smithy": "2.0",
    "shapes": {
        "test#Common": {
            "type": "structure",
            "members": {
                "description": {
                    "target": "smithy.api#String",
                    "traits": {
                        "smithy.api#required": {}
                    }
                }
            },
            "traits": {
                "smithy.api#mixin": {}
            }
        },
        "test#Str": {
            "type": "structure",
            "mixins": [
                {
                    "target": "test#Common"
                }
            ],
            "members": {}
        },
        "test#Str$description": {
            "type": "apply",
            "traits": {
                "smithy.api#default": "test"
            }
        }
    }
}

and it also loads fine if you have this one model file.

However, if you have another model file that happens to duplicate these structures, and you try to load them both during assembly, it fails:

software.amazon.smithy.model.validation.ValidatedResultException: Result contained ERROR severity validation events: 
[ERROR] test#Str: Conflicting shape definition for `test#Str` found at `test2.json [18, 21]` and `test.json [18, 21]`. Members differ: [description] vs [description] | Model test.json:18:21
	at software.amazon.smithy.model.validation.ValidatedResult.validate(ValidatedResult.java:145)
	at software.amazon.smithy.model.validation.ValidatedResult.unwrap(ValidatedResult.java:132)

That seems wrong: the model merging process is able to resolve the "conflict" once, but not twice, in what otherwise seemed like an idempotent process.

Is this something that should be fixed?

The entire process can be reproduced with the following scala-cli script:

//> using scala "3.3.1"
//> using option "-Wunused:imports"
//> using lib "software.amazon.smithy:smithy-model:1.39.1"
import software.amazon.smithy.model.shapes.SmithyIdlModelSerializer
import software.amazon.smithy.model.node.Node
import software.amazon.smithy.model.shapes.ModelSerializer
import software.amazon.smithy.model.loader.ModelAssembler
import software.amazon.smithy.model.Model
import scala.jdk.CollectionConverters._

val model =
  """$version: "2"
    |namespace test
    |
    |@mixin
    |structure Common {
    |  @required description: String
    |}
    |
    |structure Str with [Common] {
    |  description: String = "test"
    |}
    |""".stripMargin

val loaded = Model
  .assembler()
  .putProperty(ModelAssembler.DISABLE_JAR_CACHE, true)
  .addUnparsedModel(
    "test.smithy",
    model,
  )
  .assemble()
  .unwrap()

SmithyIdlModelSerializer.builder().build().serialize(loaded).asScala.head._2

val serialized = Node.prettyPrintJson(ModelSerializer.builder().build().serialize(loaded))

Model
  .assembler()
  .addUnparsedModel("test.json", serialized)
  .addUnparsedModel("test2.json", serialized)
  .assemble()
  .unwrap()
@kubukoz
Copy link
Contributor Author

kubukoz commented Oct 18, 2023

Hey team, this is causing us more trouble nowadays. Can someone please triage the issue and drop some hints for a solution so that we can contribute a fix?

@haydenbaker
Copy link
Contributor

The diff between the shape is due to the required trait from the mixin only being applied once since the trait gets “claimed” the first time the shape is built and removed from the unclaimed traits. When the shape gets built for the second time, the unclaimed traits is empty, so the member doesn’t get the required trait applied, and hence the comparison between previous and built shows a diff.

There's a few ways we could go about this, but a simple way is to track what is "claimed" in addition to "unclaimed" (and don't remove items from unclaimed), and then if anything in unclaimed is not in claimed, validation event should be emitted (in emitUnclaimedTraits).

@mtdowling mtdowling added the bug This issue is a bug. label Oct 27, 2023
milesziemer added a commit to milesziemer/smithy that referenced this issue Nov 6, 2023
Fixes smithy-lang#2004.

When an apply statement targets a mixed in member, the trait
application is stored until the mixed in member has been
synthesized. However, if you load the same model file multiple
times in the same loader, the trait application is only stored
once, and removed when it is claimed, so when de-conflicting
the duplicate mixed-in member definitions, only one of them
will have the trait applied, resulting in a conflict.

This commit updates the loader to keep track of when these
unclaimed traits have been claimed, rather than just removing
them when claimed. As long as the traits have been claimed
once, we know they aren't being applied to a non-existent
shape. This allows the traits to be claimed multiple times
if necessary.

Test cases were added for two different situations:
1. If the exact same model is loaded multiple times, so
there are multiple instances where the traits need to be
claimed. In this case there shouldn't be conflicts like
in the issue.
2. If the apply statement is loaded multiple times, but
the member it targets is only loaded once. In this case
there shouldn't be any unclaimed traits, even though
there are technically multiple of the same apply
statements.
milesziemer added a commit that referenced this issue Nov 8, 2023
Fixes #2004.

When an apply statement targets a mixed in member, the trait
application is stored until the mixed in member has been
synthesized. However, if you load the same model file multiple
times in the same loader, the trait application is only stored
once, and removed when it is claimed, so when de-conflicting
the duplicate mixed-in member definitions, only one of them
will have the trait applied, resulting in a conflict.

This commit updates the loader to keep track of when these
unclaimed traits have been claimed, rather than just removing
them when claimed. As long as the traits have been claimed
once, we know they aren't being applied to a non-existent
shape. This allows the traits to be claimed multiple times
if necessary.

Test cases were added for two different situations:
1. If the exact same model is loaded multiple times, so
there are multiple instances where the traits need to be
claimed. In this case there shouldn't be conflicts like
in the issue.
2. If the apply statement is loaded multiple times, but
the member it targets is only loaded once. In this case
there shouldn't be any unclaimed traits, even though
there are technically multiple of the same apply
statements.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug This issue is a bug.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants