Skip to content

Commit c48dd82

Browse files
committed
Fail model builds early when syntax errors occur
Continuing to parse models after a syntax error will result in many unintelligible validation events that obscure the actual syntax error. Fixes #262
1 parent eeade32 commit c48dd82

File tree

5 files changed

+45
-32
lines changed

5 files changed

+45
-32
lines changed

smithy-model/src/main/java/software/amazon/smithy/model/loader/LoaderVisitor.java

+11-7
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,6 @@ final class LoaderVisitor {
6060
private static final Logger LOGGER = Logger.getLogger(LoaderVisitor.class.getName());
6161
private static final TraitDefinition.Provider TRAIT_DEF_PROVIDER = new TraitDefinition.Provider();
6262

63-
/** Whether or not a call to {@link #onEnd()} has been made. */
64-
private boolean calledOnEnd;
65-
6663
/** Factory used to create traits. */
6764
private final TraitFactory traitFactory;
6865

@@ -96,6 +93,9 @@ final class LoaderVisitor {
9693
/** Map of shape aliases to their alias. */
9794
private final Map<String, ShapeId> useShapes = new HashMap<>();
9895

96+
/** The result that is populated when onEnd is called. */
97+
private ValidatedResult<Model> result;
98+
9999
private static final class PendingTrait {
100100
final ShapeId id;
101101
final Node value;
@@ -278,7 +278,7 @@ public <T> Optional<T> getProperty(String property, Class<T> type) {
278278
}
279279

280280
private void validateState(FromSourceLocation sourceLocation) {
281-
if (calledOnEnd) {
281+
if (result != null) {
282282
throw new IllegalStateException("Cannot call visitor method because visitor has called onEnd");
283283
}
284284
}
@@ -502,8 +502,10 @@ void onUseShape(ShapeId id, FromSourceLocation location) {
502502
* @return Returns the validated model result.
503503
*/
504504
public ValidatedResult<Model> onEnd() {
505-
validateState(SourceLocation.NONE);
506-
calledOnEnd = true;
505+
if (result != null) {
506+
return result;
507+
}
508+
507509
Model.Builder modelBuilder = Model.builder().metadata(metadata);
508510

509511
finalizeShapeTargets();
@@ -551,7 +553,9 @@ public ValidatedResult<Model> onEnd() {
551553

552554
// Add any remaining built shapes.
553555
modelBuilder.addShapes(builtShapes.values());
554-
return new ValidatedResult<>(modelBuilder.build(), events);
556+
result = new ValidatedResult<>(modelBuilder.build(), events);
557+
558+
return result;
555559
}
556560

557561
private void finalizeShapeTargets() {

smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java

+19-18
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
import software.amazon.smithy.model.validation.ValidationEvent;
4646
import software.amazon.smithy.model.validation.Validator;
4747
import software.amazon.smithy.model.validation.ValidatorFactory;
48-
import software.amazon.smithy.utils.ListUtils;
4948

5049
/**
5150
* Assembles and validates a {@link Model} from documents, files, shapes, and
@@ -448,31 +447,27 @@ public ModelAssembler removeProperty(String setting) {
448447
* and validation events.
449448
*/
450449
public ValidatedResult<Model> assemble() {
450+
LoaderVisitor visitor = createLoaderVisitor();
451+
451452
try {
452-
return doAssemble();
453+
return doAssemble(visitor);
453454
} catch (SourceException e) {
454-
return ValidatedResult.fromErrors(ListUtils.of(ValidationEvent.fromSourceException(e)));
455+
visitor.onError(ValidationEvent.fromSourceException(e));
456+
return visitor.onEnd();
455457
}
456458
}
457459

458-
private ValidatedResult<Model> doAssemble() {
460+
private LoaderVisitor createLoaderVisitor() {
459461
if (traitFactory == null) {
460462
traitFactory = LazyTraitFactoryHolder.INSTANCE;
461463
}
462464

463-
LoaderVisitor visitor = new LoaderVisitor(traitFactory, properties);
464-
465-
// Load models first to ensure a version is set.
466-
for (Map.Entry<String, Supplier<InputStream>> modelEntry : inputStreamModels.entrySet()) {
467-
if (!ModelLoader.load(modelEntry.getKey(), modelEntry.getValue(), visitor)) {
468-
LOGGER.warning(() -> "No ModelLoader was able to load " + modelEntry.getKey());
469-
}
470-
}
465+
return new LoaderVisitor(traitFactory, properties);
466+
}
471467

472-
if (!documentNodes.isEmpty()) {
473-
for (Node node : documentNodes) {
474-
ModelLoader.loadParsedNode(node, visitor);
475-
}
468+
private ValidatedResult<Model> doAssemble(LoaderVisitor visitor) {
469+
if (!disablePrelude) {
470+
mergeModelIntoVisitor(Prelude.getPreludeModel(), visitor);
476471
}
477472

478473
shapes.forEach(visitor::onShape);
@@ -482,8 +477,14 @@ private ValidatedResult<Model> doAssemble() {
482477
mergeModelIntoVisitor(model, visitor);
483478
}
484479

485-
if (!disablePrelude) {
486-
mergeModelIntoVisitor(Prelude.getPreludeModel(), visitor);
480+
for (Node node : documentNodes) {
481+
ModelLoader.loadParsedNode(node, visitor);
482+
}
483+
484+
for (Map.Entry<String, Supplier<InputStream>> modelEntry : inputStreamModels.entrySet()) {
485+
if (!ModelLoader.load(modelEntry.getKey(), modelEntry.getValue(), visitor)) {
486+
LOGGER.warning(() -> "No ModelLoader was able to load " + modelEntry.getKey());
487+
}
487488
}
488489

489490
ValidatedResult<Model> modelResult = visitor.onEnd();

smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelLoader.java

+5
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ static boolean load(String filename, Supplier<InputStream> contentSupplier, Load
6868
} else {
6969
return false;
7070
}
71+
} catch (ModelSyntaxException e) {
72+
// A syntax error in any model is going to really mess up the loading
73+
// process. While we *could* tolerate syntax errors and move on, to the
74+
// next model, doing so would likely emit many unintelligible errors.
75+
throw e;
7176
} catch (SourceException e) {
7277
visitor.onError(ValidationEvent.fromSourceException(e));
7378
return true;

smithy-model/src/test/java/software/amazon/smithy/model/loader/LoaderVisitorTest.java

+5-6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static org.hamcrest.Matchers.empty;
2020
import static org.hamcrest.Matchers.equalTo;
2121
import static org.hamcrest.Matchers.instanceOf;
22+
import static org.hamcrest.Matchers.is;
2223
import static org.hamcrest.Matchers.not;
2324
import static org.junit.jupiter.api.Assertions.assertTrue;
2425

@@ -61,12 +62,10 @@ public void cannotMutateAfterOnEnd() {
6162
}
6263

6364
@Test
64-
public void cannotCallOnEndTwice() {
65-
Assertions.assertThrows(IllegalStateException.class, () -> {
66-
LoaderVisitor visitor = new LoaderVisitor(FACTORY);
67-
visitor.onEnd();
68-
visitor.onEnd();
69-
});
65+
public void callingOnEndTwiceIsIdempotent() {
66+
LoaderVisitor visitor = new LoaderVisitor(FACTORY);
67+
68+
assertThat(visitor.onEnd(), is(visitor.onEnd()));
7069
}
7170

7271
@Test

smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -483,10 +483,14 @@ public void canLoadModelsFromJar() {
483483

484484
@Test
485485
public void gracefullyParsesPartialDocuments() {
486-
String document = "namespace foo.baz\nstring MyString\nstr";
486+
String document = "namespace foo.baz\n"
487+
+ "@required\n" // < this trait is invalid, but that's not validated due to the syntax error
488+
+ "string MyString\n"
489+
+ "str"; // < syntax error here
487490
ValidatedResult<Model> result = new ModelAssembler().addUnparsedModel("foo.smithy", document).assemble();
488491

489492
assertTrue(result.isBroken());
493+
assertThat(result.getValidationEvents(Severity.ERROR), hasSize(1));
490494
assertTrue(result.getResult().isPresent());
491495
assertTrue(result.getResult().get().getShape(ShapeId.from("foo.baz#MyString")).isPresent());
492496
}

0 commit comments

Comments
 (0)