From 712d218016524ed8d3bde739aa9fee70805bb992 Mon Sep 17 00:00:00 2001 From: "Evgeniy.Stepanov" Date: Sat, 2 May 2020 19:47:19 +0200 Subject: [PATCH] Validation for module structure in installers for thin client GitOrigin-RevId: ad2203d6e2e31fce707eb78c20121bd6d3cbd235 --- .../intellij/build/BuildOptions.groovy | 6 + .../build/impl/DistributionJARsBuilder.groovy | 15 ++ .../impl/ModuleStructureValidator.groovy | 219 ++++++++++++++++++ 3 files changed, 240 insertions(+) create mode 100644 platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/ModuleStructureValidator.groovy diff --git a/platform/build-scripts/groovy/org/jetbrains/intellij/build/BuildOptions.groovy b/platform/build-scripts/groovy/org/jetbrains/intellij/build/BuildOptions.groovy index ebc792e96915..ce50bd6a3925 100644 --- a/platform/build-scripts/groovy/org/jetbrains/intellij/build/BuildOptions.groovy +++ b/platform/build-scripts/groovy/org/jetbrains/intellij/build/BuildOptions.groovy @@ -180,6 +180,12 @@ class BuildOptions { */ String hashAlgorithm = "SHA-384" + /** + * Enables module structure validation, false by default + */ + static final String VALIDATE_MODULES_STRUCTURE = "intellij.build.module.structure" + boolean validateModuleStructure = System.getProperty(VALIDATE_MODULES_STRUCTURE, "false").toBoolean() + BuildOptions() { targetOS = System.getProperty("intellij.build.target.os") if (targetOS == OS_CURRENT) { diff --git a/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/DistributionJARsBuilder.groovy b/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/DistributionJARsBuilder.groovy index 27c527f8be6d..8f6364f0fc93 100644 --- a/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/DistributionJARsBuilder.groovy +++ b/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/DistributionJARsBuilder.groovy @@ -7,6 +7,7 @@ import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.text.StringUtil import com.intellij.util.containers.MultiMap import groovy.io.FileType +import groovy.transform.CompileStatic import org.apache.tools.ant.types.FileSet import org.apache.tools.ant.types.resources.FileProvider import org.jetbrains.annotations.Nullable @@ -239,6 +240,7 @@ class DistributionJARsBuilder { } void buildJARs() { + validateModuleStructure() prebuildSVG() buildOrderFiles() buildSearchableOptions() @@ -285,6 +287,19 @@ class DistributionJARsBuilder { }) } + /** + * Validates module structure to be ensure all module dependencies are included + */ + @CompileStatic + void validateModuleStructure() { + if (!buildContext.options.validateModuleStructure) + return + + def modules = new HashSet(platform.moduleJars.values()) + def validator = new ModuleStructureValidator(buildContext, modules) + validator.validate() + } + /** * Build index which is used to search options in the Settings dialog. */ diff --git a/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/ModuleStructureValidator.groovy b/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/ModuleStructureValidator.groovy new file mode 100644 index 000000000000..399f5694ce84 --- /dev/null +++ b/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/ModuleStructureValidator.groovy @@ -0,0 +1,219 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.intellij.build.impl + +import groovy.io.FileType +import groovy.transform.CompileStatic +import groovy.xml.QName +import org.jetbrains.intellij.build.BuildContext +import org.jetbrains.jps.model.java.JpsJavaExtensionService +import org.jetbrains.jps.model.java.impl.JpsJavaDependencyExtensionRole +import org.jetbrains.jps.model.module.JpsModule +import org.jetbrains.jps.model.module.JpsModuleDependency + +@CompileStatic +class ModuleStructureValidator { + + private static QName includeName = new QName("http://www.w3.org/2001/XInclude", "include") + private static QName fallbackName = new QName("http://www.w3.org/2001/XInclude", "fallback") + + private static HashSet pathAttributes = new HashSet([ + "interface", "implementation", "class", "topic", "instance", "provider", + "implements", "headlessImplementation", "serviceInterface", "serviceImplementation", + "implementationClass", "beanClass", "schemeClass", "factoryClass", "handlerClass", + "forClass", "className", "predicateClassName", "displayNameSupplierClassName", "preloaderClassName", + "treeRenderer"]) + + private static HashSet nonPathAttributes = new HashSet([ + "id", "value", "key", "testServiceImplementation", "defaultExtensionNs", "qualifiedName", "childrenEPName"]) + + private static HashSet pathElements = new HashSet(["interface-class", "implementation-class"]) + private static HashSet predefinedTypes = new HashSet(["java.lang.Object"]) + + private BuildContext buildContext + private HashSet moduleNames + private ArrayList errors + private ArrayList warnings + + ModuleStructureValidator(BuildContext buildContext, HashSet moduleNames) { + this.buildContext = buildContext + this.moduleNames = moduleNames + this.errors = new ArrayList<>() + this.warnings = new ArrayList<>() + } + + void validate() { + buildContext.messages.info("Validating modules...") + def visitedModules = new HashSet() + for (moduleName in moduleNames) { + validateModuleDependencies(visitedModules, buildContext.findModule(moduleName)) + } + + buildContext.messages.info("Validating xml descriptors...") + validateXmlDescriptors() + + if (warnings.isEmpty() && errors.isEmpty()) { + buildContext.messages.info("Validation finished successfully") + } + else { + if (warnings.any()) { + buildContext.messages.warning("Validation warnings: \n" + warnings.join("\n")) + } + if (errors.any()) { + buildContext.messages.warning("Validation errors: \n" + errors.join("\n")) + } + } + } + + private void validateModuleDependencies(HashSet visitedModules, JpsModule module) { + if (visitedModules.contains(module)) { + return + } + visitedModules.add(module) + + for (dependency in module.dependenciesList.dependencies) { + if (dependency instanceof JpsModuleDependency) { + // Skip test dependencies + def role = dependency.container.getChild(JpsJavaDependencyExtensionRole.INSTANCE) + if (role != null && role.scope.name() == "TEST") continue + + // Skip localization modules + def dependantModule = ((JpsModuleDependency)dependency).module + if (dependantModule.name.endsWith("resources.en")) { + continue + } + + if (!moduleNames.contains(dependantModule.name)) { + errors.add("Missing dependency found: ${module.name} -> ${dependantModule.name} [${role.scope.name()}]") + continue + } + + validateModuleDependencies(visitedModules, dependantModule) + } + } + } + + private void validateXmlDescriptors() { + def roots = new ArrayList() + for (moduleName in moduleNames) { + def module = buildContext.findModule(moduleName) + for (root in module.sourceRoots) { + roots.add(root.file) + } + } + + // Start validating from product xml descriptor + def productDescriptorName = "META-INF\\${buildContext.productProperties.platformPrefix}Plugin.xml" + def productDescriptorFile = findDescriptorFile(productDescriptorName, roots) + if (productDescriptorFile == null) { + errors.add("Can not find product descriptor $productDescriptorName") + return + } + + def allDescriptors = new HashSet() + validateXmlDescriptorsRec(productDescriptorFile, roots, allDescriptors) + validateXmlRegistrations(allDescriptors) + } + + private void validateXmlDescriptorsRec(File descriptor, ArrayList roots, HashSet allDescriptors) { + + allDescriptors.add(descriptor) + + def descriptorFiles = new ArrayList() + def xml = new XmlParser().parse(descriptor) + + def includeNodes = xml.depthFirst().findAll { it instanceof Node && ((Node)it).name() == includeName } + for (includeNode in includeNodes) { + def ref = ((Node)includeNode).attribute("href").toString() + if (ref == null) continue + + def descriptorFile = findDescriptorFile(ref, roots + descriptor.parentFile) + if (descriptorFile == null) { + def isOptional = (((Node)includeNode).children().any { it instanceof Node && ((Node)it).name() == fallbackName }) + if (isOptional) { + warnings.add("Can not find optional xml descriptor '$ref' referenced in '${descriptor.name}'") + } + else { + errors.add("Can not find xml descriptor '$ref' referenced in '${descriptor.name}'") + } + } + else { + descriptorFiles.add(descriptorFile) + } + } + + for (descriptorFile in descriptorFiles) { + validateXmlDescriptorsRec(descriptorFile, roots, allDescriptors) + } + } + + private static File findDescriptorFile(String name, List roots) { + for (root in roots) { + def descriptorFile = new File(root, name) + if (descriptorFile.exists() && descriptorFile.file) { + return descriptorFile + } + } + + return null + } + + private void validateXmlRegistrations(HashSet descriptors) { + def classes = new HashSet(predefinedTypes.collect()) + for (moduleName in moduleNames) { + def outputDirectory = JpsJavaExtensionService.instance.getOutputDirectory(buildContext.findModule(moduleName), false) + outputDirectory.eachFileRecurse(FileType.FILES) { + if (!it.name.endsWith('.class') || it.name.endsWith("Kt.class")) { + return + } + + def className = it.path + .replace(outputDirectory.path + "\\", "") + .replace(".class", "") + .replace("\\", ".") + classes.add(className) + } + } + + buildContext.messages.info("Found ${classes.size()} classes in ${moduleNames.size()} modules") + for (descriptor in descriptors) { + def xml = new XmlParser().parse(descriptor) + validateXmlRegistrationsRec(descriptor.name, xml, classes) + } + } + + private void validateXmlRegistrationsRec(String source, Node xml, HashSet classes) { + for (attribute in xml.attributes()) { + def name = attribute.key.toString() + def value = attribute.value.toString() + + if (pathAttributes.contains(name)) { + checkRegistration(source, value, classes) + continue + } + if (nonPathAttributes.contains(name)) { + continue + } + + if (value.startsWith("com.") || value.startsWith("org.")) { + warnings.add("Attribute '$name' contains qualified path '$value'. Add attribute into 'ModuleStructureValidator.pathAttributes' or 'ModuleStructureValidator.nonPathAttributes' collection.") + } + } + + for (child in xml.children()) { + if (child instanceof Node) { + for (pathElement in pathElements) { + if (!(child.name() == pathElement)) continue + checkRegistration(source, child.text(), classes) + } + + validateXmlRegistrationsRec(source, (Node)child, classes) + } + } + } + + private void checkRegistration(String source, String value, HashSet classes) { + if (value.isEmpty()) return + if (classes.contains(value)) return + errors.add("Unresolved registration '$value' in $source") + } +}