Skip to content

Commit

Permalink
Introduce ModelAwareGradleLintRule and deprecate GradleModelAware trait
Browse files Browse the repository at this point in the history
  • Loading branch information
rpalcolea committed Jan 7, 2025
1 parent b46c755 commit 24e8b51
Show file tree
Hide file tree
Showing 21 changed files with 252 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package com.netflix.nebula.lint.plugin

import com.netflix.nebula.lint.rule.GradleLintRule
import com.netflix.nebula.lint.rule.GradleModelAware
import com.netflix.nebula.lint.rule.ModelAwareGradleLintRule
import org.codenarc.rule.Rule
import org.gradle.api.Project

Expand Down Expand Up @@ -62,8 +62,8 @@ class LintRuleRegistry {
if(implClassName) {
try {
Rule r = (Rule) classLoader.loadClass(implClassName).newInstance()
if(r instanceof GradleModelAware) {
(r as GradleModelAware).project = project
if(r instanceof ModelAwareGradleLintRule) {
(r as ModelAwareGradleLintRule).project = project
}

if(r instanceof GradleLintRule) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import javax.annotation.Nullable
* Decorate lint rule visitors with this interface in order to use the
* evaluated Gradle project model in the rule
*/
@Deprecated // We suggest using ModelAwareGradleLintRule instead
trait GradleModelAware {
Project project
Map<String, List<String>> projectDefaultImports = null
Expand Down Expand Up @@ -189,21 +190,4 @@ trait GradleModelAware {
}
return null
}
}

class TypeInformation {
@Nullable
Class clazz
@Nullable
Object object

TypeInformation(Object object) {
this.object = object
this.clazz = object.class
}

TypeInformation(Object object, Class clazz) {
this.object = object
this.clazz = clazz
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,196 @@

package com.netflix.nebula.lint.rule

abstract class ModelAwareGradleLintRule extends GradleLintRule implements GradleModelAware {
import org.codehaus.groovy.ast.expr.ClosureExpression
import org.codehaus.groovy.ast.expr.ConstantExpression
import org.codehaus.groovy.ast.expr.Expression
import org.codehaus.groovy.ast.expr.MapExpression
import org.codehaus.groovy.ast.expr.MethodCallExpression
import org.codehaus.groovy.ast.expr.PropertyExpression
import org.codehaus.groovy.ast.expr.VariableExpression
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.internal.DefaultDomainObjectCollection
import org.gradle.api.plugins.ExtensionAware
import org.gradle.configuration.ImportsReader

import javax.annotation.Nullable

abstract class ModelAwareGradleLintRule extends GradleLintRule {
Project project
Map<String, List<String>> projectDefaultImports = null

TypeInformation receiver(MethodCallExpression call) {
List<Expression> fullCallStack = typedDslStack(callStack + call)
List<TypeInformation> typedStack = []
for (Expression currentMethod in fullCallStack) {
if (typedStack.empty) {
typedStack.add(new TypeInformation(project))
}
while (!typedStack.empty) {
def current = typedStack.last()
def candidate = findDirectCandidate(current, currentMethod)
if (candidate != null) {
typedStack.add(candidate)
break
}
typedStack.removeLast()
}
}
if (typedStack.size() >= 2) { //there should be the method return type and the receiver at least
return typedStack[-2]
} else {
return null
}
}

private findDirectCandidate(TypeInformation current, Expression currentExpression) {
String methodName
switch (currentExpression) {
case MethodCallExpression:
methodName = currentExpression.methodAsString
break
case PropertyExpression:
methodName = currentExpression.propertyAsString
break
case VariableExpression:
methodName = currentExpression.text
break
case ConstantExpression:
methodName = currentExpression.text
break
default:
return null
}
def getter = current.clazz.getMethods().find { it.name == "get${methodName.capitalize()}" }
if (getter != null) {
if (current.object != null) {
try {
return new TypeInformation(getter.invoke(current.object))
} catch (ignored) {
// ignore and fallback to the return type
}
}
return new TypeInformation(null, getter.returnType)
}

// there is no public API for DomainObjectCollection.type
if (current.object != null && DefaultDomainObjectCollection.class.isAssignableFrom(current.clazz)) {
def collectionItemType = ((DefaultDomainObjectCollection) current.object).type

if (methodName == "withType" && currentExpression instanceof MethodCallExpression && currentExpression.arguments.size() >= 1) {
def className = currentExpression.arguments[0]
def candidate = findSuitableClass(className.text, collectionItemType)
if (candidate != null) {
collectionItemType = candidate
}
}

if ((methodName == "create" || methodName == "register") && currentExpression instanceof MethodCallExpression && currentExpression.arguments.size() >= 2 && currentExpression.arguments[1] !instanceof ClosureExpression) {
def className = currentExpression.arguments[1]
def candidate = findSuitableClass(className.text, collectionItemType)
if (candidate != null) {
collectionItemType = candidate
}
}

def transformationOrFactoryMethod = current.clazz.getMethods().find { it.name == methodName && it.parameterTypes[-1] == Action.class }
if (transformationOrFactoryMethod != null) {
if (collectionItemType.isAssignableFrom(transformationOrFactoryMethod.returnType)) {
return new TypeInformation(null, transformationOrFactoryMethod.returnType)
} else {
// assume that all actions are done on the collection type
return new TypeInformation(null, collectionItemType)
}
}
}

// note that we can't use tasks.findByName because it may lead to unwanted side effects because of potential task creation
if (Project.class.isAssignableFrom(current.clazz) && methodName == "task" && currentExpression instanceof MethodCallExpression) {
def taskType = extractTaskType(currentExpression)
if (taskType != null) {
return new TypeInformation(null, taskType)
}
return new TypeInformation(null, Task.class)
}

def factoryMethod = current.clazz.getMethods().find { it.name == methodName && it.parameterTypes[-1] == Action.class }
if (factoryMethod != null) {
// assume that this is a factory method that returns the created type
return new TypeInformation(null, factoryMethod.returnType)
}

if (current.object != null && current.object instanceof ExtensionAware) {
def extension = current.object.extensions.findByName(methodName)
if (extension != null) {
return new TypeInformation(extension)
}
}
return null;
}

private List<Class> findClassInScope(String name) {
if (this.projectDefaultImports == null) {
this.projectDefaultImports = project.services.get(ImportsReader.class).getSimpleNameToFullClassNamesMapping()
}
return this.projectDefaultImports.get(name);
}

@Nullable
private Class findSuitableClass(String className, Class parentClass) {
def candidates = (findClassInScope(className) ?: []) + [className]
for (String candidate in candidates) {
try {
def candidateClass = Class.forName(candidate)
if (parentClass.isAssignableFrom(candidateClass)) {
return candidateClass
}
} catch (ignored) {
// ignore and try the next candidate
}
}
return null
}

@Nullable
private Class extractTaskType(MethodCallExpression currentExpression) {
for (Expression arg in currentExpression.arguments) {
if (arg instanceof VariableExpression || arg instanceof ConstantExpression) {
def candidate = findSuitableClass(arg.text, Task.class)
if (candidate != null) {
return candidate
}
} else if (arg instanceof MapExpression) {
def type = arg
.mapEntryExpressions
.find { it.keyExpression.text == "type" }
?.valueExpression?.text
if (type != null) {
def candidate = findSuitableClass(type, Task.class)
if (candidate != null) {
return candidate
}
}
}
}
return null
}
}

class TypeInformation {
@Nullable
Class clazz
@Nullable
Object object

TypeInformation(Object object) {
this.object = object
this.clazz = object.class
}

TypeInformation(Object object, Class clazz) {
this.object = object
this.clazz = clazz
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package com.netflix.nebula.lint.rule.dependency

import com.netflix.nebula.lint.rule.GradleDependency
import com.netflix.nebula.lint.rule.GradleLintRule
import com.netflix.nebula.lint.rule.GradleModelAware
import com.netflix.nebula.lint.rule.ModelAwareGradleLintRule
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.expr.MethodCallExpression
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ModuleVersionIdentifier

abstract class AbstractDuplicateDependencyClassRule extends GradleLintRule implements GradleModelAware {
abstract class AbstractDuplicateDependencyClassRule extends ModelAwareGradleLintRule {
String description = 'classpaths with duplicate classes may break unpredictably depending on the order in which dependencies are provided to the classpath'

Set<Configuration> directlyUsedConfigurations = [] as Set
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package com.netflix.nebula.lint.rule.dependency

import com.netflix.nebula.lint.rule.GradleDependency
import com.netflix.nebula.lint.rule.GradleLintRule
import com.netflix.nebula.lint.rule.GradleModelAware
import com.netflix.nebula.lint.rule.ModelAwareGradleLintRule
import groovy.transform.CompileStatic
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.expr.ArgumentListExpression
Expand All @@ -24,7 +23,7 @@ import org.slf4j.LoggerFactory

import java.util.stream.Collectors

class BypassedForcesRule extends GradleLintRule implements GradleModelAware {
class BypassedForcesRule extends ModelAwareGradleLintRule {
String description = 'remove bypassed forces and strict constraints. Works for static and ranged declarations'
Map<String, Collection<ForcedDependency>> forcedDependenciesPerProject = new HashMap<String, Collection<ForcedDependency>>()
private final DefaultVersionComparator VERSIONED_COMPARATOR = new DefaultVersionComparator()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@
package com.netflix.nebula.lint.rule.dependency

import com.netflix.nebula.lint.rule.GradleDependency
import com.netflix.nebula.lint.rule.GradleLintRule
import com.netflix.nebula.lint.rule.GradleModelAware
import com.netflix.nebula.lint.rule.ModelAwareGradleLintRule
import org.codehaus.groovy.ast.expr.ClosureExpression
import org.codehaus.groovy.ast.expr.MethodCallExpression

class DependencyParenthesesRule extends GradleLintRule implements GradleModelAware {
class DependencyParenthesesRule extends ModelAwareGradleLintRule {
String description = "don't put parentheses around dependency definitions unless it is necessary"

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ package com.netflix.nebula.lint.rule.dependency

import com.netflix.nebula.lint.rule.GradleAstUtil
import com.netflix.nebula.lint.rule.GradleDependency
import com.netflix.nebula.lint.rule.GradleLintRule
import com.netflix.nebula.lint.rule.GradleModelAware
import com.netflix.nebula.lint.rule.ModelAwareGradleLintRule
import groovy.transform.CompileStatic
import org.codehaus.groovy.ast.expr.MethodCallExpression

@CompileStatic
class DependencyTupleExpressionRule extends GradleLintRule implements GradleModelAware {
class DependencyTupleExpressionRule extends ModelAwareGradleLintRule {
String description = "use the more compact string representation of a dependency when possible"

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@ package com.netflix.nebula.lint.rule.dependency
import com.netflix.nebula.interop.GradleKt
import com.netflix.nebula.lint.GradleViolation
import com.netflix.nebula.lint.rule.GradleDependency
import com.netflix.nebula.lint.rule.GradleLintRule
import com.netflix.nebula.lint.rule.GradleModelAware
import com.netflix.nebula.lint.rule.ModelAwareGradleLintRule
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import org.codehaus.groovy.ast.expr.ConstantExpression
import org.codehaus.groovy.ast.expr.Expression
import org.codehaus.groovy.ast.expr.MethodCallExpression

@CompileStatic
class DeprecatedDependencyConfigurationRule extends GradleLintRule implements GradleModelAware {
class DeprecatedDependencyConfigurationRule extends ModelAwareGradleLintRule {
String description = 'Replace deprecated configurations in dependencies'

private final Map CONFIGURATION_REPLACEMENTS = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ package com.netflix.nebula.lint.rule.dependency

import com.netflix.nebula.lint.VersionNumber
import com.netflix.nebula.lint.rule.GradleDependency
import com.netflix.nebula.lint.rule.GradleLintRule
import com.netflix.nebula.lint.rule.GradleModelAware
import com.netflix.nebula.lint.rule.ModelAwareGradleLintRule
import groovy.transform.CompileStatic
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.ast.ClassNode
Expand All @@ -20,7 +19,7 @@ import org.gradle.api.artifacts.Configuration
*/
@Incubating
@CompileStatic
class MinimumDependencyVersionRule extends GradleLintRule implements GradleModelAware {
class MinimumDependencyVersionRule extends ModelAwareGradleLintRule {
String description = 'pull up dependencies to a minimum version if necessary'
Set alreadyAdded = [] as Set
Set<Configuration> resolvableAndResolvedConfigurations
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.netflix.nebula.lint.rule.dependency

import com.netflix.nebula.lint.rule.GradleLintRule
import com.netflix.nebula.lint.rule.GradleModelAware
import com.netflix.nebula.lint.rule.ModelAwareGradleLintRule
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.transform.TupleConstructor
Expand All @@ -11,7 +10,7 @@ import org.codehaus.groovy.ast.expr.MethodCallExpression


@CompileStatic
class MultiProjectCircularDependencyRule extends GradleLintRule implements GradleModelAware {
class MultiProjectCircularDependencyRule extends ModelAwareGradleLintRule {
String description = 'Detect circular dependencies in multi projects'

private final String PROJECT_METHOD_NAME = 'project'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@
package com.netflix.nebula.lint.rule.dependency

import com.netflix.nebula.lint.rule.GradleDependency
import com.netflix.nebula.lint.rule.GradleLintRule
import com.netflix.nebula.lint.rule.GradleModelAware
import com.netflix.nebula.lint.rule.ModelAwareGradleLintRule
import com.netflix.nebula.lint.rule.dependency.provider.MavenBomRecommendationProvider
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.expr.MethodCallExpression

class RecommendedVersionsRule extends GradleLintRule implements GradleModelAware {
class RecommendedVersionsRule extends ModelAwareGradleLintRule {
private static final String GRADLE_VERSION_WITH_EXPERIMENTAL_FEATURES = '4.5'
private static final String GRADLE_VERSION_WITH_OPT_IN_FEATURES = '4.6'
private static final String GRADLE_VERSION_WITH_DEFAULT_FEATURES = '5.0'
Expand Down
Loading

0 comments on commit 24e8b51

Please sign in to comment.