org.codenarc.rule.grails.GrailsDomainStringPropertyMaxSizeRule.groovy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of CodeNarc Show documentation
Show all versions of CodeNarc Show documentation
The CodeNarc project provides a static analysis tool for Groovy code.
/*
* Copyright 2018 the original author or authors.
*
* Licensed 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.
*/
package org.codenarc.rule.grails
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.FieldNode
import org.codehaus.groovy.ast.PropertyNode
import org.codehaus.groovy.ast.expr.ArgumentListExpression
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.ListExpression
import org.codehaus.groovy.ast.expr.MapEntryExpression
import org.codehaus.groovy.ast.expr.MapExpression
import org.codehaus.groovy.ast.expr.MethodCallExpression
import org.codehaus.groovy.ast.expr.NamedArgumentListExpression
import org.codehaus.groovy.ast.expr.TupleExpression
import org.codehaus.groovy.ast.stmt.BlockStatement
import org.codehaus.groovy.ast.stmt.ExpressionStatement
import org.codenarc.rule.AbstractAstVisitorRule
import org.codenarc.rule.AbstractAstVisitor
import java.lang.reflect.Modifier
/**
* String properties in Grails domain classes have to define maximum size otherwise the property is mapped to VARCHAR(255) causing runtime exceptions to occur
*
* @author Vladimir Orany
*/
class GrailsDomainStringPropertyMaxSizeRule extends AbstractAstVisitorRule {
String name = 'GrailsDomainStringPropertyMaxSize'
int priority = 2
Class astVisitorClass = GrailsDomainStringPropertyMaxSizeAstVisitor
String applyToFilesMatching = GrailsUtil.DOMAIN_FILES
}
class GrailsDomainStringPropertyMaxSizeAstVisitor extends AbstractAstVisitor {
private final Map constraintsAndMappings = [:]
.withDefault { new ClassConstraintsAndMapping() }
@Override
void visitField(FieldNode node) {
ClassConstraintsAndMapping constraintsAndMapping = constraintsAndMappings[node.declaringClass.name]
if (node.name == 'constraints') {
constraintsAndMapping.constraints = getMethodCallsAndMapKeys(node.initialValueExpression)
super.visitField(node)
} else if (node.name == 'mapping') {
constraintsAndMapping.mapping = getMethodCallsAndMapKeys(node.initialValueExpression)
super.visitField(node)
}
}
@Override
void visitProperty(PropertyNode node) {
ClassConstraintsAndMapping constraintsAndMapping = constraintsAndMappings[node.declaringClass.name]
if (node.type.name == 'String'
&& !Modifier.isStatic(node.modifiers)
&& !Modifier.isTransient(node.modifiers)) {
constraintsAndMapping.stringProperties.add(node.name)
}
super.visitProperty(node)
}
@Override
protected void visitClassComplete(ClassNode node) {
super.visitClassComplete(node)
ClassConstraintsAndMapping constraintsAndMapping = constraintsAndMappings[node.name]
constraintsAndMapping.stringProperties.each {
Set constraint = constraintsAndMapping.constraints[it]
Set mapping = constraintsAndMapping.mapping[it]
if (!constraint?.intersect(['maxSize', 'size']) && !mapping?.contains('type')) {
addViolation(node.getProperty(it), "There is no constraint on the size of String property '$it' which will result in applying database defaults")
}
}
}
private static Map> getMethodCallsAndMapKeys(Expression expression) {
if (!expression || !(expression instanceof ClosureExpression)) {
return Collections.emptyMap()
}
ClosureExpression closure = expression as ClosureExpression
if (!closure.code || !(closure.code instanceof BlockStatement)) {
return Collections.emptyMap()
}
Map> result = [:].withDefault { [] as Set }
BlockStatement code = closure.code as BlockStatement
code.statements.findAll {
it instanceof ExpressionStatement && it.expression instanceof MethodCallExpression
}.each { ExpressionStatement exp ->
collectMapKeys(exp.expression as MethodCallExpression, result)
}
result
}
private static void collectMapKeys(MethodCallExpression methodCallExpression, Map> result) {
String methodName = methodCallExpression.methodAsString
if (methodName == 'importFrom') {
collectIncludedProperties(methodCallExpression).each {
// assume size constraint applied if the constraints are imported from different class
// as the source class is also validated for the presence of the size constraint
result[it].add('size')
}
} else if (methodCallExpression.arguments instanceof TupleExpression) {
TupleExpression arguments = methodCallExpression.arguments as TupleExpression
if (arguments.expressions.size() == 1 && arguments.expressions.first() instanceof NamedArgumentListExpression) {
NamedArgumentListExpression namedArgumentListExpression = arguments.expressions.first() as NamedArgumentListExpression
result[methodName].addAll namedArgumentListExpression.mapEntryExpressions*.keyExpression*.text
}
}
}
private static Set collectIncludedProperties(MethodCallExpression call) {
if (call.arguments instanceof ArgumentListExpression) {
ArgumentListExpression argumentList = call.arguments as ArgumentListExpression
MapExpression mapExpression = argumentList.expressions.find { it instanceof MapExpression }
if (mapExpression) {
return collectIncludedProperties(mapExpression)
}
}
return Collections.emptySet()
}
private static Set collectIncludedProperties(MapExpression mapExpression) {
MapEntryExpression entryExpression = mapExpression.mapEntryExpressions.find {
it.keyExpression.text == 'include'
}
if (entryExpression.valueExpression instanceof ListExpression) {
ListExpression includesList = entryExpression.valueExpression as ListExpression
return includesList.expressions.findAll { it instanceof ConstantExpression }*.text
}
return Collections.emptySet()
}
}
class ClassConstraintsAndMapping {
Map> constraints = [:].withDefault { [] as Set }
Map> mapping = [:].withDefault { [] as Set }
Set stringProperties = []
}