borinquenkid commented on code in PR #16048: URL: https://github.com/apache/grails-core/pull/16048#discussion_r3887712962
########## build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovyDslConfigurationMetadataParser.groovy: ########## @@ -0,0 +1,207 @@ +/* + * 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 + * + * https://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.apache.grails.buildsrc + +import org.codehaus.groovy.ast.ModuleNode +import org.codehaus.groovy.ast.expr.ArgumentListExpression +import org.codehaus.groovy.ast.expr.BinaryExpression +import org.codehaus.groovy.ast.expr.ClassExpression +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.MapExpression +import org.codehaus.groovy.ast.expr.MethodCallExpression +import org.codehaus.groovy.ast.expr.PropertyExpression +import org.codehaus.groovy.ast.expr.TernaryExpression +import org.codehaus.groovy.ast.expr.VariableExpression +import org.codehaus.groovy.ast.stmt.BlockStatement +import org.codehaus.groovy.ast.stmt.ExpressionStatement +import org.codehaus.groovy.ast.stmt.IfStatement +import org.codehaus.groovy.ast.stmt.Statement +import org.codehaus.groovy.control.CompilationFailedException +import org.codehaus.groovy.control.SourceUnit +import org.codehaus.groovy.syntax.Types + +import java.nio.charset.StandardCharsets + +/** Extracts configuration metadata from Groovy DSL source without evaluating source code. */ +final class GroovyDslConfigurationMetadataParser { + + private GroovyDslConfigurationMetadataParser() { + } + + static List<Map<String, Object>> parse(Collection<File> files, Map<String, String> rootPrefixes) { + List<Map<String, Object>> properties = [] + files.findAll { File file -> file.isFile() }.sort { File file -> file.absolutePath }.each { File file -> + parseFile(file, rootPrefixes, properties) + } + properties.sort { Map<String, Object> property -> property.name as String } + } + + private static void parseFile(File file, Map<String, String> rootPrefixes, + List<Map<String, Object>> properties) { + ModuleNode module = parseSource(file) + module.statementBlock.statements.each { Statement statement -> + MethodCallExpression call = methodCall(statement) + String root = call?.methodAsString + ClosureExpression closure = call == null ? null : closureArgument(call) + String prefix = root == null ? null : rootPrefixes[root] + if (prefix != null && closure != null) { + parseStatements(closure.code, prefix, false, properties) + } + } + } + + private static ModuleNode parseSource(File file) { + try { + SourceUnit source = SourceUnit.create(file.absolutePath, file.getText(StandardCharsets.UTF_8.name())) + source.parse() + source.completePhase() + source.nextPhase() + source.convert() + source.errorCollector.failIfErrors() + source.AST + } catch (CompilationFailedException exception) { + throw new IllegalArgumentException("Failed to parse Groovy DSL source '${file.absolutePath}'", exception) + } + } + + private static void parseStatements(Statement statement, String prefix, boolean conditional, + List<Map<String, Object>> properties) { + if (statement instanceof BlockStatement) { + statement.statements.each { Statement child -> parseStatements(child, prefix, conditional, properties) } + } else if (statement instanceof IfStatement) { + parseStatements(statement.ifBlock, prefix, true, properties) + parseStatements(statement.elseBlock, prefix, true, properties) + } else if (statement instanceof ExpressionStatement) { + Expression expression = statement.expression + if (expression instanceof BinaryExpression && expression.operation.type == Types.ASSIGN) { + addAssignment(expression, prefix, conditional, properties) + } else if (expression instanceof MethodCallExpression) { + ClosureExpression closure = closureArgument(expression) + String nestedName = expression.methodAsString + if (closure != null && nestedName != null) { + parseStatements(closure.code, "${prefix}.${nestedName}", conditional, properties) + } + } + } + } + + private static void addAssignment(BinaryExpression assignment, String prefix, boolean conditional, + List<Map<String, Object>> properties) { + List<String> segments = leftHandPath(assignment.leftExpression) + if (segments == null) { + return + } + String name = "${prefix}.${segments.join('.')}" + Inference inference = infer(assignment.rightExpression) + Map<String, Object> property = [name: name] + if (inference.type != null) { + property.type = inference.type + } + if (!conditional && inference.literal) { + property.defaultValue = inference.value + } + properties << property Review Comment: Fixed in dd1ab42936 + 6c3328e3df. Same-name entries are now grouped and reconciled before they reach `indexByName`: unconditional entries are checked for uniqueness among themselves specifically (order-independent, so a conditional entry sitting between two conflicting unconditional assignments can't mask the conflict), the surviving unconditional entry's `defaultValue` is kept, and the type is unioned across every entry for the name — covers both examples above (differing literal types, and an unconditional default overridden under `Environment.TEST`). Added specs for both, plus a null-branch variant and a "still fails on two genuinely conflicting unconditional assignments" case. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
