Skip to content

Commit 3f26a93

Browse files
Add a root meta-trait to declare root shapes
This adds a new `root` meta-trait. This trait marks a trait as a rooting trait. Any shape marked by a rooting trait is considered to be a root shape. Root shapes are shapes that are always considered to be referenced. Currently, service shapes and traits are considered to be root shapes. A new metadata trait is being added that will also define new root shapes. Beyond that, this trait is a necessary prerequisite for using Smithy to define schemas and use-cases that are not bound to the context of a service API. For example, imagine a theoretical `jsonSchema` trait that uses Smithy to define a plain JSONSchema definition that isn't tied to OpenAPI. We wouldn't want those shapes to be considered unreferenced because that might mean they get filtered away inappropriately and it would mean the unreferenced shape linter would start producing a huge amount of noise.
1 parent da5120c commit 3f26a93

18 files changed

Lines changed: 229 additions & 12 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "feature",
3+
"description": "Added a new `root` meta-trait that marks a trait as a rooting trait. Shapes targeted by a rooting trait are considered root shapes, which are always considered to be referenced.",
4+
"pull_requests": [
5+
"[#3079](https://github.com/smithy-lang/smithy/pull/3079)"
6+
]
7+
}

docs/source-2.0/guides/model-linters.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ configuration is provided, the linter will check if a shape is connected to
7878
the closure of any service shape. A selector can be provided to define a
7979
custom set of "root" shapes to customize how the linter determines if a shape
8080
is unreferenced. Shapes that are connected through the :ref:`idref-trait`
81-
are considered connected.
81+
are considered connected. Shapes targeted by traits marked with the
82+
:ref:`root-trait` are also considered connected.
8283

8384
Rationale
8485
Just like unused variables in code, removing unused shapes from a model

docs/source-2.0/spec/model.rst

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,7 @@ Is changed to:
11231123
Then the change to the ``foo`` member from "a" to "b" is backward
11241124
incompatible, as is the removal of the ``baz`` member.
11251125

1126+
11261127
Referring to list members
11271128
^^^^^^^^^^^^^^^^^^^^^^^^^
11281129

@@ -1250,6 +1251,58 @@ backward incompatible.
12501251
effect.
12511252

12521253

1254+
.. smithy-trait:: smithy.api#root
1255+
.. _root-trait:
1256+
1257+
``root`` trait
1258+
--------------
1259+
1260+
Summary
1261+
A meta-trait that marks a trait as a *rooting* trait. Shapes targeted by
1262+
a rooting trait, and all shapes transitively connected to them, are
1263+
considered root shapes and are never treated as unreferenced.
1264+
Trait selector
1265+
``[trait|trait]``
1266+
Value type
1267+
Annotation trait
1268+
1269+
Certain shapes in a Smithy model are considered *root shapes*. Root shapes
1270+
sit at the top of a closure of shapes that serve some shared purpose. For
1271+
example, :ref:`service shapes <service>` are root shapes because they are
1272+
the entry point of a service API. Root shapes and all shapes transitively
1273+
connected to them are always considered referenced.
1274+
1275+
The :ref:`trait <trait-trait>` trait is itself marked with ``@root``, which
1276+
is why all trait definitions and the shapes they reference are automatically
1277+
considered root shapes. The ``@root`` trait allows custom traits to designate
1278+
their targets as additional root shapes. This is useful when shapes are not
1279+
connected to a service but should still be retained in the model.
1280+
1281+
The following example defines a ``@myRoot`` trait that is marked with
1282+
``@root``. Any shape that has ``@myRoot`` applied, along with all shapes
1283+
transitively connected to it, will never be considered unreferenced:
1284+
1285+
.. code-block:: smithy
1286+
1287+
@root
1288+
@trait
1289+
structure myRoot {}
1290+
1291+
@myRoot
1292+
structure MyShape {
1293+
value: MyString
1294+
}
1295+
1296+
// Not unreferenced because it is connected to MyShape,
1297+
// which is a root shape due to @myRoot.
1298+
string MyString
1299+
1300+
.. seealso::
1301+
1302+
:ref:`UnreferencedShape`
1303+
The linter that detects shapes not connected to any root shape.
1304+
1305+
12531306
.. _prelude:
12541307

12551308
-------

smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import software.amazon.smithy.model.selector.Selector;
1414
import software.amazon.smithy.model.shapes.Shape;
1515
import software.amazon.smithy.model.shapes.ShapeId;
16-
import software.amazon.smithy.model.traits.TraitDefinition;
16+
import software.amazon.smithy.model.traits.RootTrait;
1717
import software.amazon.smithy.utils.FunctionalUtils;
1818

1919
/**
@@ -25,6 +25,9 @@
2525
* that considers every matching shape a root shape. For example, a model might consider all shapes marked with
2626
* a trait called "root" to be a root shape.
2727
*
28+
* <p>Root shapes will also always include any shapes targeted by a trait that has the
29+
* {@link software.amazon.smithy.model.traits.RootTrait} and any shape that is connected to one of those shapes.
30+
*
2831
* <p>Prelude shapes are never considered unreferenced.
2932
*/
3033
public final class UnreferencedShapes {
@@ -77,9 +80,12 @@ public Set<Shape> compute(Model model) {
7780
shapeWalker.iterateShapes(root, traversed).forEachRemaining(shape -> connected.add(shape.getId()));
7881
}
7982

80-
// Don't remove shapes that are traits or connected to traits.
81-
for (Shape trait : model.getShapesWithTrait(TraitDefinition.class)) {
82-
shapeWalker.iterateShapes(trait, traversed).forEachRemaining(shape -> connected.add(shape.getId()));
83+
// Don't remove root shapes or shapes that ar connected to them. This includes traits.
84+
for (Shape rootTrait : model.getShapesWithTrait(RootTrait.class)) {
85+
for (Shape shape : model.getShapesWithTrait(rootTrait.getId())) {
86+
shapeWalker.iterateShapes(shape, traversed)
87+
.forEachRemaining(s -> connected.add(s.getId()));
88+
}
8389
}
8490

8591
// Any shape that wasn't identified as connected to a root is considered unreferenced.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.model.traits;
6+
7+
import software.amazon.smithy.model.node.Node;
8+
import software.amazon.smithy.model.node.ObjectNode;
9+
import software.amazon.smithy.model.shapes.ShapeId;
10+
11+
/**
12+
* Indicates that the targeted trait is a rooting trait. Shapes targeted by a
13+
* rooting trait are considered root shapes. Root shapes and shapes transitively
14+
* connected to them are never considered to be unreferenced.
15+
*/
16+
public final class RootTrait extends AnnotationTrait {
17+
public static final ShapeId ID = ShapeId.from("smithy.api#root");
18+
19+
public RootTrait(ObjectNode node) {
20+
super(ID, node);
21+
}
22+
23+
public RootTrait() {
24+
this(Node.objectNode());
25+
}
26+
27+
public static final class Provider extends AnnotationTrait.Provider<RootTrait> {
28+
public Provider() {
29+
super(ID, RootTrait::new);
30+
}
31+
}
32+
}

smithy-model/src/main/java/software/amazon/smithy/model/validation/linters/UnreferencedShapeValidator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public List<ValidationEvent> validate(Model model) {
8181
for (Shape shape : new UnreferencedShapes(config.rootShapeSelector).compute(model)) {
8282
events.add(note(shape,
8383
"This shape is unreferenced. It has no modeled connections to shapes "
84+
+ "targeted by a root trait or shapes "
8485
+ "that match the following selector: `" + config.rootShapeSelector + "`"));
8586
}
8687

smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ software.amazon.smithy.model.traits.RequiredTrait$Provider
5858
software.amazon.smithy.model.traits.RequiresLengthTrait$Provider
5959
software.amazon.smithy.model.traits.ResourceIdentifierTrait$Provider
6060
software.amazon.smithy.model.traits.RetryableTrait$Provider
61+
software.amazon.smithy.model.traits.RootTrait$Provider
6162
software.amazon.smithy.model.traits.SensitiveTrait$Provider
6263
software.amazon.smithy.model.traits.SinceTrait$Provider
6364
software.amazon.smithy.model.traits.SparseTrait$Provider

smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ structure Unit {}
6363
// --- Shapes below are traits and the private shapes that define them.
6464

6565
/// Makes a shape a trait.
66+
@root
6667
@trait(
6768
selector: ":is(simpleType, list, map, structure, union)"
6869
breakingChanges: [
@@ -156,6 +157,22 @@ enum StructurallyExclusive {
156157
TARGET = "target"
157158
}
158159

160+
/// Marks a trait as a rooting trait.
161+
///
162+
/// Shapes targeted by a rooting trait are considered root shapes. Root shapes
163+
/// and shapes transitively connected to them are never considered to be
164+
/// unreferenced.
165+
@trait(
166+
selector: "[trait|trait]"
167+
breakingChanges: [
168+
{
169+
change: "presence"
170+
severity: "DANGER"
171+
}
172+
]
173+
)
174+
structure root {}
175+
159176
/// Marks a shape or member as deprecated.
160177
@trait
161178
structure deprecated {

smithy-model/src/test/java/software/amazon/smithy/model/neighbor/UnreferencedShapesTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,27 @@ public void doesNotCheckShapeReferencesThroughIdRefOnUnconnectedShapes() {
9898
ShapeId.from("com.foo#Referenced"),
9999
ShapeId.from("com.foo#Unconnected")));
100100
}
101+
102+
@Test
103+
public void rootTraitMarksTargetedShapesAsConnected() {
104+
Model m = Model.assembler()
105+
.addUnparsedModel("test.smithy",
106+
"$version: \"2.0\"\n"
107+
+ "namespace ns.foo\n"
108+
+ "@root\n"
109+
+ "@trait\n"
110+
+ "structure myRoot {}\n"
111+
+ "@myRoot\n"
112+
+ "string Rooted\n"
113+
+ "string Unconnected\n")
114+
.assemble()
115+
.unwrap();
116+
117+
Set<ShapeId> ids = new UnreferencedShapes().compute(m)
118+
.stream()
119+
.map(Shape::getId)
120+
.collect(Collectors.toSet());
121+
122+
assertThat(ids, containsInAnyOrder(ShapeId.from("ns.foo#Unconnected")));
123+
}
101124
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.model.traits;
6+
7+
import static org.hamcrest.MatcherAssert.assertThat;
8+
import static org.hamcrest.Matchers.equalTo;
9+
import static org.hamcrest.Matchers.instanceOf;
10+
import static org.junit.jupiter.api.Assertions.assertTrue;
11+
12+
import java.util.Optional;
13+
import org.junit.jupiter.api.Test;
14+
import software.amazon.smithy.model.node.Node;
15+
import software.amazon.smithy.model.shapes.ShapeId;
16+
17+
public class RootTraitTest {
18+
@Test
19+
public void loadsTrait() {
20+
TraitFactory provider = TraitFactory.createServiceFactory();
21+
Optional<Trait> trait = provider.createTrait(
22+
ShapeId.from("smithy.api#root"),
23+
ShapeId.from("ns.qux#foo"),
24+
Node.objectNode());
25+
26+
assertTrue(trait.isPresent());
27+
assertThat(trait.get(), instanceOf(RootTrait.class));
28+
assertThat(trait.get().toNode(), equalTo(Node.objectNode()));
29+
}
30+
}

0 commit comments

Comments
 (0)