[MENFORCER-423] Add rule to enforce an explicit dependency scope

diff --git a/enforcer-rules/src/main/java/org/apache/maven/plugins/enforcer/AbstractStandardEnforcerRule.java b/enforcer-rules/src/main/java/org/apache/maven/plugins/enforcer/AbstractStandardEnforcerRule.java
index fecdb09..f369259 100644
--- a/enforcer-rules/src/main/java/org/apache/maven/plugins/enforcer/AbstractStandardEnforcerRule.java
+++ b/enforcer-rules/src/main/java/org/apache/maven/plugins/enforcer/AbstractStandardEnforcerRule.java
@@ -21,6 +21,8 @@
 
 import org.apache.maven.enforcer.rule.api.EnforcerLevel;
 import org.apache.maven.enforcer.rule.api.EnforcerRule2;
+import org.apache.maven.model.InputLocation;
+import org.apache.maven.project.MavenProject;
 
 /**
  * The Class AbstractStandardEnforcerRule.
@@ -60,4 +62,70 @@
         this.level = level;
     }
 
+    /**
+     * Returns an identifier of a given project.
+     * @param project the project
+     * @return the identifier of the project in the format {@code <groupId>:<artifactId>:<version>}
+     */
+    private static String getProjectId( MavenProject project )
+    {
+        StringBuilder buffer = new StringBuilder( 128 );
+    
+        buffer.append( ( project.getGroupId() != null && project.getGroupId().length() > 0 ) ? project.getGroupId()
+                        : "[unknown-group-id]" );
+        buffer.append( ':' );
+        buffer.append( ( project.getArtifactId() != null && project.getArtifactId().length() > 0 )
+                        ? project.getArtifactId()
+                        : "[unknown-artifact-id]" );
+        buffer.append( ':' );
+        buffer.append( ( project.getVersion() != null && project.getVersion().length() > 0 ) ? project.getVersion()
+                        : "[unknown-version]" );
+    
+        return buffer.toString();
+    }
+
+    /**
+     * Creates a string with line/column information for problems originating directly from this POM. Inspired by
+     * {@code o.a.m.model.building.ModelProblemUtils.formatLocation(...)}.
+     *
+     * @param project the current project.
+     * @param location The location which should be formatted, must not be {@code null}.
+     * @return The formatted problem location or an empty string if unknown, never {@code null}.
+     */
+    protected static String formatLocation( MavenProject project, InputLocation location )
+    {
+        StringBuilder buffer = new StringBuilder();
+    
+        if ( !location.getSource().getModelId().equals( getProjectId( project ) ) )
+        {
+            buffer.append( location.getSource().getModelId() );
+    
+            if ( location.getSource().getLocation().length() > 0 )
+            {
+                if ( buffer.length() > 0 )
+                {
+                    buffer.append( ", " );
+                }
+                buffer.append( location.getSource().getLocation() );
+            }
+        }
+        if ( location.getLineNumber() > 0 )
+        {
+            if ( buffer.length() > 0 )
+            {
+                buffer.append( ", " );
+            }
+            buffer.append( "line " ).append( location.getLineNumber() );
+        }
+        if ( location.getColumnNumber() > 0 )
+        {
+            if ( buffer.length() > 0 )
+            {
+                buffer.append( ", " );
+            }
+            buffer.append( "column " ).append( location.getColumnNumber() );
+        }
+        return buffer.toString();
+    }
+
 }
diff --git a/enforcer-rules/src/main/java/org/apache/maven/plugins/enforcer/BanDependencyManagementScope.java b/enforcer-rules/src/main/java/org/apache/maven/plugins/enforcer/BanDependencyManagementScope.java
index 94fe177..06b9461 100644
--- a/enforcer-rules/src/main/java/org/apache/maven/plugins/enforcer/BanDependencyManagementScope.java
+++ b/enforcer-rules/src/main/java/org/apache/maven/plugins/enforcer/BanDependencyManagementScope.java
@@ -29,7 +29,6 @@
 import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
 import org.apache.maven.model.Dependency;
 import org.apache.maven.model.DependencyManagement;
-import org.apache.maven.model.InputLocation;
 import org.apache.maven.plugin.logging.Log;
 import org.apache.maven.plugins.enforcer.utils.ArtifactMatcher;
 import org.apache.maven.project.MavenProject;
@@ -81,7 +80,6 @@
                 List<Dependency> violatingDependencies  = getViolatingDependencies( logger, depMgmt );
                 if ( !violatingDependencies.isEmpty() )
                 {
-                    String projectId = getProjectId( project );
                     String message = getMessage();
                     StringBuilder buf = new StringBuilder();
                     if ( message == null )
@@ -91,7 +89,7 @@
                     buf.append( message + System.lineSeparator() );
                     for ( Dependency violatingDependency : violatingDependencies )
                     {
-                        buf.append( getErrorMessage( projectId, violatingDependency ) );
+                        buf.append( getErrorMessage( project, violatingDependency ) );
                     }
                     throw new EnforcerRuleException( buf.toString() );
                 }
@@ -136,76 +134,14 @@
         return violatingDependencies;
     }
 
-    private static CharSequence getErrorMessage( String projectId, Dependency violatingDependency )
+    private static CharSequence getErrorMessage( MavenProject project, Dependency violatingDependency )
     {
         return "Banned scope '" + violatingDependency.getScope() + "' used on dependency '"
                         + violatingDependency.getManagementKey() + "' @ "
-                        + formatLocation( projectId, violatingDependency.getLocation( "" ) )
+                        + formatLocation( project, violatingDependency.getLocation( "" ) )
                         + System.lineSeparator();
     }
 
-    // Get the identifier of the POM in the format <groupId>:<artifactId>:<version>.
-    protected static String getProjectId( MavenProject project )
-    {
-        StringBuilder buffer = new StringBuilder( 128 );
-
-        buffer.append( ( project.getGroupId() != null && project.getGroupId().length() > 0 ) ? project.getGroupId()
-                        : "[unknown-group-id]" );
-        buffer.append( ':' );
-        buffer.append( ( project.getArtifactId() != null && project.getArtifactId().length() > 0 )
-                        ? project.getArtifactId()
-                        : "[unknown-artifact-id]" );
-        buffer.append( ':' );
-        buffer.append( ( project.getVersion() != null && project.getVersion().length() > 0 ) ? project.getVersion()
-                        : "[unknown-version]" );
-
-        return buffer.toString();
-    }
-
-    /**
-     * Creates a string with line/column information for problems originating directly from this POM. Inspired by
-     * {@code o.a.m.model.building.ModelProblemUtils.formatLocation(...)}.
-     *
-     * @param projectId the id of the current project's pom.
-     * @param location The location which should be formatted, must not be {@code null}.
-     * @return The formatted problem location or an empty string if unknown, never {@code null}.
-     */
-    protected static String formatLocation( String projectId, InputLocation location )
-    {
-        StringBuilder buffer = new StringBuilder();
-
-        if ( !location.getSource().getModelId().equals( projectId ) )
-        {
-            buffer.append( location.getSource().getModelId() );
-
-            if ( location.getSource().getLocation().length() > 0 )
-            {
-                if ( buffer.length() > 0 )
-                {
-                    buffer.append( ", " );
-                }
-                buffer.append( location.getSource().getLocation() );
-            }
-        }
-        if ( location.getLineNumber() > 0 )
-        {
-            if ( buffer.length() > 0 )
-            {
-                buffer.append( ", " );
-            }
-            buffer.append( "line " ).append( location.getLineNumber() );
-        }
-        if ( location.getColumnNumber() > 0 )
-        {
-            if ( buffer.length() > 0 )
-            {
-                buffer.append( ", " );
-            }
-            buffer.append( "column " ).append( location.getColumnNumber() );
-        }
-        return buffer.toString();
-    }
-
     public void setExcludes( List<String> theExcludes )
     {
         this.excludes = theExcludes;
diff --git a/enforcer-rules/src/main/java/org/apache/maven/plugins/enforcer/RequireExplicitDependencyScope.java b/enforcer-rules/src/main/java/org/apache/maven/plugins/enforcer/RequireExplicitDependencyScope.java
new file mode 100644
index 0000000..2a5b96e
--- /dev/null
+++ b/enforcer-rules/src/main/java/org/apache/maven/plugins/enforcer/RequireExplicitDependencyScope.java
@@ -0,0 +1,97 @@
+package org.apache.maven.plugins.enforcer;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.text.ChoiceFormat;
+import java.util.List;
+
+import org.apache.maven.enforcer.rule.api.EnforcerLevel;
+import org.apache.maven.enforcer.rule.api.EnforcerRule2;
+import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
+import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
+import org.apache.maven.model.Dependency;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.shared.utils.logging.MessageBuilder;
+import org.apache.maven.shared.utils.logging.MessageUtils;
+import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
+
+/**
+ * Checks that all dependencies have an explicitly declared scope in the non-effective pom (i.e. without taking
+ * inheritance or dependency management into account).
+ */
+public class RequireExplicitDependencyScope
+    extends AbstractNonCacheableEnforcerRule
+    implements EnforcerRule2
+{
+
+    @Override
+    public void execute( EnforcerRuleHelper helper )
+        throws EnforcerRuleException
+    {
+        try
+        {
+            int numMissingDependencyScopes = 0;
+            MavenProject project = (MavenProject) helper.evaluate( "${project}" );
+            if ( project == null )
+            {
+                throw new ExpressionEvaluationException( "${project} is null" );
+            }
+            List<Dependency> dependencies = project.getOriginalModel().getDependencies(); // this is the non-effective
+                                                                                          // model but the original one
+                                                                                          // without inheritance and
+                                                                                          // interpolation resolved
+            // check scope without considering inheritance
+            for ( Dependency dependency : dependencies )
+            {
+                helper.getLog().debug( "Found dependency " + dependency );
+                if ( dependency.getScope() == null )
+                {
+                    MessageBuilder msgBuilder = MessageUtils.buffer();
+                    msgBuilder
+                        .a( "Dependency " ).strong( dependency.getManagementKey() )
+                        .a( " @ " ).strong( formatLocation( project, dependency.getLocation( "" ) ) )
+                        .a( " does not have an explicit scope defined!" ).toString();
+                    if ( getLevel() == EnforcerLevel.ERROR )
+                    {
+                        helper.getLog().error( msgBuilder.toString() );
+                    }
+                    else
+                    {
+                        helper.getLog().warn( msgBuilder.toString() );
+                    }
+                    numMissingDependencyScopes++;
+                }
+            }
+            if ( numMissingDependencyScopes > 0 )
+            {
+                ChoiceFormat scopesFormat = new ChoiceFormat( "1#scope|1<scopes" );
+                String logCategory = getLevel() == EnforcerLevel.ERROR ? "errors" : "warnings";
+                throw new EnforcerRuleException( "Found " + numMissingDependencyScopes + " missing dependency "
+                    + scopesFormat.format( numMissingDependencyScopes )
+                    + ". Look at the " + logCategory + " emitted above for the details." );
+            }
+        }
+        catch ( ExpressionEvaluationException eee )
+        {
+            throw new EnforcerRuleException( "Cannot resolve expression: " + eee.getCause(), eee );
+        }
+    }
+
+}
diff --git a/enforcer-rules/src/site/apt/index.apt b/enforcer-rules/src/site/apt/index.apt
index c56d818..dfd8dec 100644
--- a/enforcer-rules/src/site/apt/index.apt
+++ b/enforcer-rules/src/site/apt/index.apt
@@ -56,7 +56,9 @@
   * {{{./requireActiveProfile.html}requireActiveProfile}} - enforces one or more active profiles.
   
   * {{{./requireEnvironmentVariable.html}requireEnvironmentVariable}} - enforces the existence of an environment variable.
-  
+
+  * {{{./requireExplicitDependencyScope.html}requireExplicitDependencyScope}} - enforces that all dependencies have an explicit scope.
+
   * {{{./requireFileChecksum.html}requireFileChecksum}} - enforces that the specified file has a certain checksum.
   
   * {{{./requireFilesDontExist.html}requireFilesDontExist}} - enforces that the list of files does not exist.
diff --git a/enforcer-rules/src/site/apt/requireExplicitDependencyScope.apt.vm b/enforcer-rules/src/site/apt/requireExplicitDependencyScope.apt.vm
new file mode 100644
index 0000000..14d3b92
--- /dev/null
+++ b/enforcer-rules/src/site/apt/requireExplicitDependencyScope.apt.vm
@@ -0,0 +1,62 @@
+~~ Licensed to the Apache Software Foundation (ASF) under one
+~~ or more contributor license agreements.  See the NOTICE file
+~~ distributed with this work for additional information
+~~ regarding copyright ownership.  The ASF licenses this file
+~~ to you under the Apache License, Version 2.0 (the
+~~ "License"); you may not use this file except in compliance
+~~ with the License.  You may obtain a copy of the License at
+~~
+~~ http://www.apache.org/licenses/LICENSE-2.0
+~~
+~~ Unless required by applicable law or agreed to in writing,
+~~ software distributed under the License is distributed on an
+~~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+~~ KIND, either express or implied.  See the License for the
+~~ specific language governing permissions and limitations
+~~ under the License.    
+ 
+  ------
+  Require Explicit Dependency Scope
+  ------
+  Konrad Windszus
+  ------
+  2022-08-03
+  ------
+
+Require Explicit Dependency Scope
+
+  This rule enforces that all dependencies have an explicitly declared scope in the non-effective pom (i.e. without taking inheritance or dependency management into account).
+  Useful when the scope is no longer part of the <<<dependencyManagement>>> or in general to force making developers a distinct decision (prevents the default scope <<<compile>>> being used for test dependencies by accident)
+
+  The rule does not support parameters.
+
+  Sample Plugin Configuration:
+  
++---+
+<project>
+  [...]
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+        <version>${project.version}</version>
+        <executions>
+          <execution>
+            <id>require-explicit-dependency-scope</id>
+            <goals>
+              <goal>enforce</goal>
+            </goals>
+            <configuration>
+              <rules>
+                <requireExplicitDependencyScope />
+              </rules>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+  [...]
+</project>
++---+
diff --git a/maven-enforcer-plugin/src/it/projects/require-dependency-scope/invoker.properties b/maven-enforcer-plugin/src/it/projects/require-dependency-scope/invoker.properties
new file mode 100644
index 0000000..e64d99e
--- /dev/null
+++ b/maven-enforcer-plugin/src/it/projects/require-dependency-scope/invoker.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+# 
+#   http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+invoker.buildResult = failure
diff --git a/maven-enforcer-plugin/src/it/projects/require-dependency-scope/pom.xml b/maven-enforcer-plugin/src/it/projects/require-dependency-scope/pom.xml
new file mode 100644
index 0000000..c80b13d
--- /dev/null
+++ b/maven-enforcer-plugin/src/it/projects/require-dependency-scope/pom.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements. See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership. The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License. You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied. See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.apache.maven.its.enforcer</groupId>
+    <artifactId>ban-dependency-management-scope-fail-test</artifactId>
+    <version>1.0</version>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-enforcer-plugin</artifactId>
+                <version>@project.version@</version>
+                <executions>
+                    <execution>
+                        <id>require-dependency-scope</id>
+                        <goals>
+                            <goal>enforce</goal>
+                        </goals>
+                        <configuration>
+                            <rules>
+                                <requireExplicitDependencyScope />
+                            </rules>
+                            <fail>true</fail>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+    
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.apache.jackrabbit.vault</groupId>
+                <artifactId>vault-cli</artifactId>
+                <version>3.6.0</version>
+                <scope>provided</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <!-- missing explicit scope -->
+        <dependency>
+            <groupId>org.apache.jackrabbit.vault</groupId>
+            <artifactId>vault-cli</artifactId>
+        </dependency>
+
+        <!-- test scope -->
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest</artifactId>
+            <version>2.2</version>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+</project>
\ No newline at end of file
diff --git a/maven-enforcer-plugin/src/it/projects/require-dependency-scope/verify.groovy b/maven-enforcer-plugin/src/it/projects/require-dependency-scope/verify.groovy
new file mode 100644
index 0000000..f4871fd
--- /dev/null
+++ b/maven-enforcer-plugin/src/it/projects/require-dependency-scope/verify.groovy
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+File buildLog = new File(basedir, 'build.log')
+assert buildLog.text.contains('[ERROR] Dependency org.apache.jackrabbit.vault:vault-cli:jar @ line 65, column 21 does not have an explicit scope defined!')
+assert buildLog.text.contains('Found 1 missing dependency scope. Look at the errors emitted above for the details.')