Add StatementSourceException 82/109582/13
authorRobert Varga <robert.varga@pantheon.tech>
Tue, 2 Jan 2024 05:38:15 +0000 (06:38 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Tue, 2 Jan 2024 14:07:42 +0000 (15:07 +0100)
There is a missing link between in model.api.meta, which is an exception
that carries a StatementSourceReference where it occurred.

JIRA: YANGTOOLS-1150
Change-Id: I75c08b90e049b620ba532163694102ef94bbd5a8
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
model/yang-model-api/src/main/java/org/opendaylight/yangtools/yang/model/api/meta/StatementSourceException.java [new file with mode: 0644]
parser/yang-parser-impl/src/main/java/org/opendaylight/yangtools/yang/parser/repo/AssembleSources.java
parser/yang-parser-impl/src/main/java/org/opendaylight/yangtools/yang/parser/repo/DependencyResolver.java
parser/yang-parser-impl/src/main/java/org/opendaylight/yangtools/yang/parser/repo/YangTextSchemaContextResolver.java
parser/yang-parser-impl/src/test/java/org/opendaylight/yangtools/yang/parser/repo/DependencyResolverTest.java
parser/yang-parser-rfc7950/src/test/java/org/opendaylight/yangtools/yang/parser/rfc7950/stmt/path/PathExpressionParserTest.java
parser/yang-parser-spi/src/main/java/org/opendaylight/yangtools/yang/parser/spi/source/SourceException.java
yang/yang-repo-api/src/main/java/org/opendaylight/yangtools/yang/model/repo/api/SchemaResolutionException.java

diff --git a/model/yang-model-api/src/main/java/org/opendaylight/yangtools/yang/model/api/meta/StatementSourceException.java b/model/yang-model-api/src/main/java/org/opendaylight/yangtools/yang/model/api/meta/StatementSourceException.java
new file mode 100644 (file)
index 0000000..9f05648
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.model.api.meta;
+
+import static java.util.Objects.requireNonNull;
+
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamException;
+import org.eclipse.jdt.annotation.NonNull;
+
+/**
+ * An exception identifying a problem detected at a particular YANG statement. Exposes {@link #sourceRef()} to that
+ * statement.
+ */
+public class StatementSourceException extends RuntimeException {
+    @java.io.Serial
+    private static final long serialVersionUID = 2L;
+
+    private final @NonNull StatementSourceReference sourceRef;
+
+    public StatementSourceException(final StatementSourceReference sourceRef, final String message) {
+        super(message);
+        this.sourceRef = requireNonNull(sourceRef);
+    }
+
+    public StatementSourceException(final StatementSourceReference sourceRef, final String message,
+            final Throwable cause) {
+        super(message, cause);
+        this.sourceRef = requireNonNull(sourceRef);
+    }
+
+    public StatementSourceException(final StatementSourceReference sourceRef, final String format,
+            final Object... args) {
+        super(format.formatted(args));
+        this.sourceRef = requireNonNull(sourceRef);
+    }
+
+    /**
+     * Return the reference to the source which caused this exception.
+     *
+     * @return the reference to the source which caused this exception
+     */
+    public final @NonNull StatementSourceReference sourceRef() {
+        return sourceRef;
+    }
+
+    @java.io.Serial
+    private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException {
+        throwNSE();
+    }
+
+    @java.io.Serial
+    private void readObjectNoData() throws ObjectStreamException {
+        throwNSE();
+    }
+
+    @java.io.Serial
+    private void writeObject(final ObjectOutputStream stream) throws IOException {
+        throwNSE();
+    }
+
+    protected final void throwNSE() throws NotSerializableException {
+        throw new NotSerializableException(getClass().getName());
+    }
+}
index 33d647c49a7d530e16397a7d4667bc46d4cd661c..fc40a2f63a5ec0bcec30bc975a29c090bc62bcc4 100644 (file)
@@ -7,22 +7,19 @@
  */
 package org.opendaylight.yangtools.yang.parser.repo;
 
-import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFluentFuture;
-
 import com.google.common.base.Function;
 import com.google.common.collect.Maps;
 import com.google.common.util.concurrent.AsyncFunction;
 import com.google.common.util.concurrent.FluentFuture;
 import java.io.IOException;
 import java.util.List;
-import java.util.Map;
 import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.util.concurrent.FluentFutures;
 import org.opendaylight.yangtools.yang.ir.YangIRSchemaSource;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 import org.opendaylight.yangtools.yang.model.api.source.SourceIdentifier;
 import org.opendaylight.yangtools.yang.model.repo.api.SchemaContextFactoryConfiguration;
 import org.opendaylight.yangtools.yang.model.repo.api.SchemaResolutionException;
-import org.opendaylight.yangtools.yang.parser.api.YangParser;
 import org.opendaylight.yangtools.yang.parser.api.YangParserException;
 import org.opendaylight.yangtools.yang.parser.api.YangParserFactory;
 import org.opendaylight.yangtools.yang.parser.api.YangSyntaxErrorException;
@@ -48,26 +45,24 @@ final class AssembleSources implements AsyncFunction<List<YangIRSchemaSource>, E
     }
 
     @Override
-    public FluentFuture<EffectiveModelContext> apply(final List<YangIRSchemaSource> sources)
-            throws SchemaResolutionException, ReactorException {
-        final Map<SourceIdentifier, YangIRSchemaSource> srcs = Maps.uniqueIndex(sources, getIdentifier);
-        final Map<SourceIdentifier, YangModelDependencyInfo> deps =
-                Maps.transformValues(srcs, YangModelDependencyInfo::forIR);
-
+    public FluentFuture<EffectiveModelContext> apply(final List<YangIRSchemaSource> sources) {
+        final var srcs = Maps.uniqueIndex(sources, getIdentifier);
+        final var deps = Maps.transformValues(srcs, YangModelDependencyInfo::forIR);
         LOG.debug("Resolving dependency reactor {}", deps);
 
-        final DependencyResolver res = switch (config.getStatementParserMode()) {
+        final var res = switch (config.getStatementParserMode()) {
             case DEFAULT_MODE -> RevisionDependencyResolver.create(deps);
         };
 
-        if (!res.getUnresolvedSources().isEmpty()) {
-            LOG.debug("Omitting models {} due to unsatisfied imports {}", res.getUnresolvedSources(),
-                res.getUnsatisfiedImports());
-            throw new SchemaResolutionException("Failed to resolve required models",
-                    res.getResolvedSources(), res.getUnsatisfiedImports());
+        final var unresolved = res.unresolvedSources();
+        if (!unresolved.isEmpty()) {
+            LOG.debug("Omitting models {} due to unsatisfied imports {}", unresolved, res.unsatisfiedImports());
+            return FluentFutures.immediateFailedFluentFuture(
+                new SchemaResolutionException("Failed to resolve required models", unresolved.get(0),
+                    res.resolvedSources(), res.unsatisfiedImports()));
         }
 
-        final YangParser parser = parserFactory.createParser(res.parserConfig());
+        final var parser = parserFactory.createParser(res.parserConfig());
         config.getSupportedFeatures().ifPresent(parser::setSupportedFeatures);
         config.getModulesDeviatedByModules().ifPresent(parser::setModulesWithSupportedDeviations);
 
@@ -76,7 +71,8 @@ final class AssembleSources implements AsyncFunction<List<YangIRSchemaSource>, E
                 parser.addSource(entry.getValue());
             } catch (YangSyntaxErrorException | IOException e) {
                 final var sourceId = entry.getKey();
-                throw new SchemaResolutionException("Failed to add source " + sourceId, sourceId, e);
+                return FluentFutures.immediateFailedFluentFuture(
+                    new SchemaResolutionException("Failed to add source " + sourceId, sourceId, e));
             }
         }
 
@@ -84,12 +80,10 @@ final class AssembleSources implements AsyncFunction<List<YangIRSchemaSource>, E
         try {
             schemaContext = parser.buildEffectiveModel();
         } catch (final YangParserException e) {
-            if (e.getCause() instanceof ReactorException re) {
-                throw new SchemaResolutionException("Failed to resolve required models", re.getSourceIdentifier(), e);
-            }
-            throw new SchemaResolutionException("Failed to resolve required models", e);
+            return FluentFutures.immediateFailedFluentFuture(e.getCause() instanceof ReactorException re
+                ? new SchemaResolutionException("Failed to resolve required models", re.getSourceIdentifier(), re) : e);
         }
 
-        return immediateFluentFuture(schemaContext);
+        return FluentFutures.immediateFluentFuture(schemaContext);
     }
 }
index c7df113fa9c0188f3c5b736e7d5abd73f36602aa..ba06ab4d64646bbeea3950b99c54dbac98185114 100644 (file)
@@ -11,15 +11,11 @@ import com.google.common.base.MoreObjects;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMultimap;
-import com.google.common.collect.Multimap;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Optional;
-import java.util.Set;
 import org.opendaylight.yangtools.yang.common.Revision;
 import org.opendaylight.yangtools.yang.common.UnresolvedQName.Unqualified;
 import org.opendaylight.yangtools.yang.model.api.ModuleImport;
@@ -27,6 +23,7 @@ import org.opendaylight.yangtools.yang.model.api.source.SourceIdentifier;
 import org.opendaylight.yangtools.yang.model.api.stmt.ImportEffectiveStatement;
 import org.opendaylight.yangtools.yang.parser.api.YangParserConfiguration;
 import org.opendaylight.yangtools.yang.parser.rfc7950.repo.YangModelDependencyInfo;
+import org.opendaylight.yangtools.yang.parser.rfc7950.repo.YangModelDependencyInfo.SubmoduleDependencyInfo;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -41,45 +38,43 @@ import org.slf4j.LoggerFactory;
 //        as new models are added to a schema context.
 abstract class DependencyResolver {
     private static final Logger LOG = LoggerFactory.getLogger(DependencyResolver.class);
+
     private final ImmutableList<SourceIdentifier> resolvedSources;
     private final ImmutableList<SourceIdentifier> unresolvedSources;
     private final ImmutableMultimap<SourceIdentifier, ModuleImport> unsatisfiedImports;
 
     protected DependencyResolver(final Map<SourceIdentifier, YangModelDependencyInfo> depInfo) {
-        final Collection<SourceIdentifier> resolved = new ArrayList<>(depInfo.size());
-        final Collection<SourceIdentifier> pending = new ArrayList<>(depInfo.keySet());
-        final Map<SourceIdentifier, BelongsToDependency> submodules = new HashMap<>();
+        final var resolved = new ArrayList<SourceIdentifier>(depInfo.size());
+        final var pending = new ArrayList<>(depInfo.keySet());
+        final var submodules = new HashMap<SourceIdentifier, BelongsToDependency>();
 
         boolean progress;
         do {
             progress = false;
 
-            final Iterator<SourceIdentifier> it = pending.iterator();
+            final var it = pending.iterator();
             while (it.hasNext()) {
-                final SourceIdentifier id = it.next();
-                final YangModelDependencyInfo dep = depInfo.get(id);
-
-                boolean okay = true;
-
-                final Set<ModuleImport> dependencies = dep.getDependencies();
+                final var sourceId = it.next();
+                final var dep = depInfo.get(sourceId);
 
                 // in case of submodule, remember belongs to
-                if (dep instanceof YangModelDependencyInfo.SubmoduleDependencyInfo) {
-                    final var parent = ((YangModelDependencyInfo.SubmoduleDependencyInfo) dep).getParentModule();
-                    submodules.put(id, new BelongsToDependency(parent));
+                if (dep instanceof SubmoduleDependencyInfo submodule) {
+                    final var parent = submodule.getParentModule();
+                    submodules.put(sourceId, new BelongsToDependency(parent));
                 }
 
-                for (final ModuleImport mi : dependencies) {
-                    if (!isKnown(resolved, mi)) {
-                        LOG.debug("Source {} is missing import {}", id, mi);
+                boolean okay = true;
+                for (var dependency : dep.getDependencies()) {
+                    if (!isKnown(resolved, dependency)) {
+                        LOG.debug("Source {} is missing import {}", sourceId, dependency);
                         okay = false;
                         break;
                     }
                 }
 
                 if (okay) {
-                    LOG.debug("Resolved source {}", id);
-                    resolved.add(id);
+                    LOG.debug("Resolved source {}", sourceId);
+                    resolved.add(sourceId);
                     it.remove();
                     progress = true;
                 }
@@ -87,22 +82,21 @@ abstract class DependencyResolver {
         } while (progress);
 
         /// Additional check only for belongs-to statement
-        for (final Entry<SourceIdentifier, BelongsToDependency> submodule : submodules.entrySet()) {
-            final BelongsToDependency belongs = submodule.getValue();
-            final SourceIdentifier sourceIdentifier = submodule.getKey();
+        for (var submodule : submodules.entrySet()) {
+            final var sourceId = submodule.getKey();
+            final var belongs = submodule.getValue();
             if (!isKnown(resolved, belongs)) {
-                LOG.debug("Source {} is missing parent {}", sourceIdentifier, belongs);
-                pending.add(sourceIdentifier);
-                resolved.remove(sourceIdentifier);
+                LOG.debug("Source {} is missing parent {}", sourceId, belongs);
+                pending.add(sourceId);
+                resolved.remove(sourceId);
             }
         }
 
-        final Multimap<SourceIdentifier, ModuleImport> imports = ArrayListMultimap.create();
-        for (final SourceIdentifier id : pending) {
-            final YangModelDependencyInfo dep = depInfo.get(id);
-            for (final ModuleImport mi : dep.getDependencies()) {
-                if (!isKnown(pending, mi) && !isKnown(resolved, mi)) {
-                    imports.put(id, mi);
+        final var imports = ArrayListMultimap.<SourceIdentifier, ModuleImport>create();
+        for (var sourceId : pending) {
+            for (var dependency : depInfo.get(sourceId).getDependencies()) {
+                if (!isKnown(pending, dependency) && !isKnown(resolved, dependency)) {
+                    imports.put(sourceId, dependency);
                 }
             }
         }
@@ -119,14 +113,14 @@ abstract class DependencyResolver {
     /**
      * Collection of sources which have been resolved.
      */
-    Collection<SourceIdentifier> getResolvedSources() {
+    ImmutableList<SourceIdentifier> resolvedSources() {
         return resolvedSources;
     }
 
     /**
      * Collection of sources which have not been resolved due to missing dependencies.
      */
-    Collection<SourceIdentifier> getUnresolvedSources() {
+    ImmutableList<SourceIdentifier> unresolvedSources() {
         return unresolvedSources;
     }
 
@@ -149,7 +143,7 @@ abstract class DependencyResolver {
      * A->C and B->C will be reported.
      * </li></ul>
      */
-    Multimap<SourceIdentifier, ModuleImport> getUnsatisfiedImports() {
+    ImmutableMultimap<SourceIdentifier, ModuleImport> unsatisfiedImports() {
         return unsatisfiedImports;
     }
 
index 06365a8cbf61ea4ce7589c717474839ef26f9fbb..42b839e1cc8b635ac08980538a938fc50ecd1f51 100644 (file)
@@ -338,14 +338,14 @@ public final class YangTextSchemaContextResolver implements AutoCloseable, Schem
     }
 
     @Beta
-    public EffectiveModelContext trySchemaContext() throws SchemaResolutionException {
+    public EffectiveModelContext trySchemaContext() throws SchemaResolutionException, ExecutionException {
         return trySchemaContext(StatementParserMode.DEFAULT_MODE);
     }
 
     @Beta
     @SuppressWarnings("checkstyle:avoidHidingCauseException")
     public EffectiveModelContext trySchemaContext(final StatementParserMode statementParserMode)
-            throws SchemaResolutionException {
+            throws SchemaResolutionException, ExecutionException {
         final var future = repository
                 .createEffectiveModelContextFactory(config(statementParserMode, getSupportedFeatures()))
                 .createEffectiveModelContext(ImmutableSet.copyOf(requiredSources));
@@ -355,11 +355,10 @@ public final class YangTextSchemaContextResolver implements AutoCloseable, Schem
         } catch (InterruptedException e) {
             throw new IllegalStateException("Interrupted while waiting for SchemaContext assembly", e);
         } catch (ExecutionException e) {
-            final var cause = e.getCause();
-            if (cause instanceof SchemaResolutionException resolutionException) {
-                throw resolutionException;
+            if (e.getCause() instanceof SchemaResolutionException sre) {
+                throw sre;
             }
-            throw new SchemaResolutionException("Failed to assemble SchemaContext", e);
+            throw e;
         }
     }
 
index 381584ec0746781eee996dd7dfe2052a26257b39..b931ea40ad929725c660be3ac8ca0043a7ca6e37 100644 (file)
@@ -27,8 +27,8 @@ public class DependencyResolverTest {
         addToMap(map, "/no-revision/top@2012-10-10.yang");
 
         final var resolved = RevisionDependencyResolver.create(map);
-        assertEquals(0, resolved.getUnresolvedSources().size());
-        assertEquals(0, resolved.getUnsatisfiedImports().size());
+        assertEquals(0, resolved.unresolvedSources().size());
+        assertEquals(0, resolved.unsatisfiedImports().size());
     }
 
     @Test
@@ -40,9 +40,9 @@ public class DependencyResolverTest {
         addToMap(map, "/model/baz.yang");
 
         final var resolved = RevisionDependencyResolver.create(map);
-        assertEquals(2, resolved.getResolvedSources().size());
-        assertEquals(1, resolved.getUnresolvedSources().size());
-        assertEquals(0, resolved.getUnsatisfiedImports().size());
+        assertEquals(2, resolved.resolvedSources().size());
+        assertEquals(1, resolved.unresolvedSources().size());
+        assertEquals(0, resolved.unsatisfiedImports().size());
     }
 
     @Test
@@ -54,9 +54,9 @@ public class DependencyResolverTest {
         addToMap(map, "/model/baz.yang");
 
         final var resolved = RevisionDependencyResolver.create(map);
-        assertEquals(0, resolved.getUnresolvedSources().size());
-        assertEquals(0, resolved.getUnsatisfiedImports().size());
-        assertEquals(4, resolved.getResolvedSources().size());
+        assertEquals(0, resolved.unresolvedSources().size());
+        assertEquals(0, resolved.unsatisfiedImports().size());
+        assertEquals(4, resolved.resolvedSources().size());
     }
 
     private static void addToMap(final Map<SourceIdentifier, YangModelDependencyInfo> map, final String yangFileName)
index 27ac70e6f6fcb5ee6fbc2abeab4b6ec53880189b..cbac3a44ab47cd5d94292e31fcd924c44588ac42 100644 (file)
@@ -71,7 +71,7 @@ public class PathExpressionParserTest {
     @Test
     void testInvalidLeftParent() {
         final var ex = assertThrows(SourceException.class, () -> parser.parseExpression(ctx, "foo("));
-        assertSame(ref, ex.getSourceReference());
+        assertSame(ref, ex.sourceRef());
         assertThat(ex.getMessage(), allOf(
             startsWith("extraneous input '(' expecting "),
             containsString(" at 1:3 [at ")));
@@ -80,7 +80,7 @@ public class PathExpressionParserTest {
     @Test
     void testInvalidRightParent() {
         final var ex = assertThrows(SourceException.class, () -> parser.parseExpression(ctx, "foo)"));
-        assertSame(ref, ex.getSourceReference());
+        assertSame(ref, ex.sourceRef());
         assertThat(ex.getMessage(), allOf(
             startsWith("extraneous input ')' expecting "),
             containsString(" at 1:3 [at ")));
@@ -89,7 +89,7 @@ public class PathExpressionParserTest {
     @Test
     void testInvalidIdentifier() {
         final var ex = assertThrows(SourceException.class, () -> parser.parseExpression(ctx, "foo%"));
-        assertSame(ref, ex.getSourceReference());
+        assertSame(ref, ex.sourceRef());
         assertThat(ex.getMessage(), startsWith("token recognition error at: '%' at 1:3 [at "));
     }
 
index 99f2bdfd7e54f6c8213197217fa5184a31cdac27..7bc37cff85dd0d279371b05e5c696a26d44772a9 100644 (file)
@@ -9,32 +9,29 @@ package org.opendaylight.yangtools.yang.parser.spi.source;
 
 import static java.util.Objects.requireNonNull;
 
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import java.util.Optional;
 import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.yang.model.api.meta.StatementSourceException;
 import org.opendaylight.yangtools.yang.model.api.meta.StatementSourceReference;
 import org.opendaylight.yangtools.yang.parser.spi.meta.CommonStmtCtx;
 
 /**
  * Thrown to indicate error in YANG model source.
  */
-public class SourceException extends RuntimeException {
+public class SourceException extends StatementSourceException {
+    @java.io.Serial
     private static final long serialVersionUID = 1L;
 
-    @SuppressFBWarnings(value = "SE_BAD_FIELD", justification = "Interface-specified member")
-    private final @NonNull StatementSourceReference sourceRef;
-
     /**
      * Create a new instance with the specified message and source. The message will be appended with
      * the source reference.
      *
      * @param message Context message
-     * @param source Statement source
+     * @param sourceRef Statement source
      */
-    public SourceException(final @NonNull String message, final @NonNull StatementSourceReference source) {
-        super(createMessage(message, source));
-        sourceRef = source;
+    public SourceException(final @NonNull String message, final @NonNull StatementSourceReference sourceRef) {
+        super(sourceRef, createMessage(message, sourceRef));
     }
 
     /**
@@ -42,13 +39,12 @@ public class SourceException extends RuntimeException {
      * the source reference.
      *
      * @param message Context message
-     * @param source Statement source
+     * @param sourceRef Statement source
      * @param cause Underlying cause of this exception
      */
-    public SourceException(final @NonNull String message, final @NonNull StatementSourceReference source,
+    public SourceException(final @NonNull String message, final @NonNull StatementSourceReference sourceRef,
             final Throwable cause) {
-        super(createMessage(message, source), cause);
-        sourceRef = source;
+        super(sourceRef, createMessage(message, sourceRef), cause);
     }
 
     /**
@@ -127,15 +123,6 @@ public class SourceException extends RuntimeException {
         this(stmt.sourceReference(), cause, format, args);
     }
 
-    /**
-     * Return the reference to the source which caused this exception.
-     *
-     * @return Source reference
-     */
-    public @NonNull StatementSourceReference getSourceReference() {
-        return sourceRef;
-    }
-
     /**
      * Throw an instance of this exception if an expression evaluates to true. If the expression evaluates to false,
      * this method does nothing.
index b2862b6ac7287ea9cfc49f80615d8ba046dcf313..2c62d018805238cffd017c9c0c62ca8cbabc10bc 100644 (file)
@@ -29,23 +29,15 @@ public class SchemaResolutionException extends SchemaSourceException {
     private final @NonNull ImmutableMultimap<SourceIdentifier, ModuleImport> unsatisfiedImports;
     private final @NonNull ImmutableList<SourceIdentifier> resolvedSources;
 
-    public SchemaResolutionException(final @NonNull String message) {
-        this(message, null);
-    }
-
-    public SchemaResolutionException(final @NonNull String message, final Throwable cause) {
-        this(message, null, cause, ImmutableList.of(), ImmutableMultimap.of());
-    }
-
     public SchemaResolutionException(final @NonNull String message, final SourceIdentifier failedSource,
             final Throwable cause) {
         this(message, failedSource, cause, ImmutableList.of(), ImmutableMultimap.of());
     }
 
-    public SchemaResolutionException(final @NonNull String message,
+    public SchemaResolutionException(final @NonNull String message, final SourceIdentifier failedSource,
             final @NonNull Collection<SourceIdentifier> resolvedSources,
             final @NonNull Multimap<SourceIdentifier, ModuleImport> unsatisfiedImports) {
-        this(message, null, null, resolvedSources, unsatisfiedImports);
+        this(message, failedSource, null, resolvedSources, unsatisfiedImports);
     }
 
     public SchemaResolutionException(final @NonNull String message, final SourceIdentifier failedSource,