Skip to content

Commit

Permalink
Evaluate constant expressions in version strings
Browse files Browse the repository at this point in the history
A version string can contain a compile-time constant expression,
currently this just fails to read the version at all.

This adds support for evaluation of such expressions and refactor the
SourceCodeAnalyzerPlugin into smaller parts.

(cherry picked from commit 41fe987)
  • Loading branch information
laeubi committed Feb 14, 2025
1 parent 7b86f6d commit f301484
Show file tree
Hide file tree
Showing 6 changed files with 422 additions and 133 deletions.
32 changes: 32 additions & 0 deletions tycho-bndlib/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,36 @@
<artifactId>org.eclipse.jdt.core</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>biz.aQute.bnd</groupId>
<artifactId>bnd-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>bnd-process</goal>
</goals>
<configuration>
<bnd>
<![CDATA[
Export-Package: org.eclipse.tycho.bndlib.*;-noimport:=true
]]>
</bnd>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*******************************************************************************
* Copyright (c) 2025 Christoph Läubrich and others.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Christoph Läubrich - initial API and implementation
*******************************************************************************/
package org.eclipse.tycho.bndlib;

import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.PackageDeclaration;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.tycho.bndlib.source.SourceCodeResolver;
import org.eclipse.tycho.bndlib.source.SourceFile;

import aQute.bnd.header.Attrs;
import aQute.bnd.osgi.Analyzer;
import aQute.bnd.osgi.Clazz;
import aQute.bnd.osgi.Descriptors.PackageRef;
import aQute.bnd.osgi.FileResource;

public class SourceCodeAnalyzer implements FileVisitor<Path> {

private static final String PACKAGE_INFO = "package-info";
private static final String ANNOTATION_VERSION = "org.osgi.annotation.versioning.Version";
private static final String ANNOTATION_EXPORT = "org.osgi.annotation.bundle.Export";
private static final String PACKAGE_INFO_JAVA = PACKAGE_INFO + SourceFile.JAVA_EXTENSION;
private static final String PACKAGE_INFO_CLASS = PACKAGE_INFO + ".class";

private Set<String> seenPackages = new HashSet<>();
private Set<Path> analyzedPath = new HashSet<>();
Map<PackageRef, Clazz> packageInfoMap = new HashMap<>();
private Analyzer analyzer;
private SourceCodeResolver typeResolver;

public SourceCodeAnalyzer(Analyzer analyzer, SourceCodeResolver typeResolver) {
this.analyzer = analyzer;
this.typeResolver = typeResolver;
}

@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String fileName = file.getFileName().toString().toLowerCase();
if (fileName.endsWith(SourceFile.JAVA_EXTENSION)) {
boolean packageInfo = fileName.equals(PACKAGE_INFO_JAVA);
if (packageInfo || analyzedPath.add(file.getParent())) {
analyzeSourceFile(file, packageInfo);
}
}
return FileVisitResult.CONTINUE;
}

private void analyzeSourceFile(Path file, boolean packageInfo) throws IOException {
SourceFile source = typeResolver.getCompilationUnit(file);
PackageDeclaration packageDecl = source.getPackage();
if (packageDecl != null) {
String packageFqdn = packageDecl.getName().getFullyQualifiedName();
PackageRef packageRef = analyzer.getPackageRef(packageFqdn);
if (seenPackages.add(packageFqdn)) {
// make the package available to bnd analyzer
analyzer.getContained().put(packageRef);
}
if (packageInfo) {
JDTClazz clazz = new JDTClazz(analyzer, packageRef.getBinary() + "/" + PACKAGE_INFO_CLASS,
new FileResource(file), analyzer.getTypeRef(packageRef.getBinary() + "/" + PACKAGE_INFO));
for (Object raw : packageDecl.annotations()) {
if (raw instanceof Annotation annot) {
Name typeName = annot.getTypeName();
String annotationFqdn = typeName.getFullyQualifiedName();
if (source.isType(annotationFqdn, ANNOTATION_EXPORT)) {
clazz.addAnnotation(analyzer.getTypeRef(ANNOTATION_EXPORT.replace('.', '/')));
packageInfoMap.put(packageRef, clazz);
} else if (source.isType(annotationFqdn, ANNOTATION_VERSION)) {
String version = getVersionFromAnnotation(annot, source);
if (version != null) {
// if the package is exported or not, the version info must be propagated
analyzer.getContained().put(packageRef, Attrs.create("version", version));
}
}
}
}
}
}
}

private String getVersionFromAnnotation(Annotation annot, SourceFile source) {
if (annot instanceof NormalAnnotation normal) {
for (Object vp : normal.values()) {
MemberValuePair pair = (MemberValuePair) vp;
if ("value".equals(pair.getName().getFullyQualifiedName())) {
return source.resolve(pair.getValue());
}
}
} else if (annot instanceof SingleMemberAnnotation single) {
return source.resolve(single.getValue());
}
return null;
}

@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,54 +13,29 @@
package org.eclipse.tycho.bndlib;

import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.PackageDeclaration;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.tycho.bndlib.source.SourceCodeResolver;

import aQute.bnd.header.Attrs;
import aQute.bnd.osgi.Analyzer;
import aQute.bnd.osgi.Builder;
import aQute.bnd.osgi.Clazz;
import aQute.bnd.osgi.Descriptors.PackageRef;
import aQute.bnd.osgi.FileResource;
import aQute.bnd.service.AnalyzerPlugin;

/**
* Enhances the analyzed classes by information obtained from the source code
*/
public class SourceCodeAnalyzerPlugin implements AnalyzerPlugin {

private static final String JAVA_EXTENSION = ".java";
private static final String PACKAGE_INFO = "package-info";
private static final String ANNOTATION_VERSION = "org.osgi.annotation.versioning.Version";
private static final String ANNOTATION_EXPORT = "org.osgi.annotation.bundle.Export";
private static final String PACKAGE_INFO_JAVA = PACKAGE_INFO + JAVA_EXTENSION;
private static final String PACKAGE_INFO_CLASS = PACKAGE_INFO + ".class";
private List<Path> sourcePaths;
private Map<PackageRef, Clazz> packageInfoMap = new HashMap<>();

private boolean alreadyRun;

private SourceCodeAnalyzer codeAnalyzer;

public SourceCodeAnalyzerPlugin() {
this(null);
}
Expand All @@ -75,94 +50,10 @@ public boolean analyzeJar(Analyzer analyzer) throws Exception {
return false;
}
alreadyRun = true;
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
Set<String> seenPackages = new HashSet<>();
Set<Path> analyzedPath = new HashSet<>();
for (Path sourcePath : getSourcePath(analyzer)) {
Files.walkFileTree(sourcePath, new FileVisitor<Path>() {

@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String fileName = file.getFileName().toString().toLowerCase();
if (fileName.endsWith(JAVA_EXTENSION)) {
boolean packageInfo = fileName.equals(PACKAGE_INFO_JAVA);
if (packageInfo || analyzedPath.add(file.getParent())) {
String source = Files.readString(file);
parser.setSource(source.toCharArray());
ASTNode ast = parser.createAST(null);
if (ast instanceof CompilationUnit cu) {
PackageDeclaration packageDecl = cu.getPackage();
if (packageDecl != null) {
List<?> imports = cu.imports();
String packageFqdn = packageDecl.getName().getFullyQualifiedName();
PackageRef packageRef = analyzer.getPackageRef(packageFqdn);
if (seenPackages.add(packageFqdn)) {
// make the package available to bnd analyzer
analyzer.getContained().put(packageRef);
}
if (packageInfo) {
JDTClazz clazz = new JDTClazz(analyzer,
packageRef.getBinary() + "/" + PACKAGE_INFO_CLASS,
new FileResource(file),
analyzer.getTypeRef(packageRef.getBinary() + "/" + PACKAGE_INFO));
// check for export annotations
boolean export = false;
String version = null;
for (Object raw : packageDecl.annotations()) {
if (raw instanceof Annotation annot) {
Name typeName = annot.getTypeName();
String annotationFqdn = typeName.getFullyQualifiedName();
if (isType(annotationFqdn, ANNOTATION_EXPORT, imports)) {
export = true;
clazz.addAnnotation(
analyzer.getTypeRef(ANNOTATION_EXPORT.replace('.', '/')));
} else if (isType(annotationFqdn, ANNOTATION_VERSION, imports)) {
if (annot instanceof NormalAnnotation normal) {
for (Object vp : normal.values()) {
MemberValuePair pair = (MemberValuePair) vp;
if ("value"
.equals(pair.getName().getFullyQualifiedName())) {
StringLiteral value = (StringLiteral) pair.getValue();
version = value.getLiteralValue();
}
}
} else if (annot instanceof SingleMemberAnnotation single) {
StringLiteral value = (StringLiteral) single.getValue();
version = value.getLiteralValue();
}
}
}
}
if (version != null) {
// if the package is exported or not, the version info must be propagated
analyzer.getContained().put(packageRef, Attrs.create("version", version));
}
if (export) {
packageInfoMap.put(packageRef, clazz);
}
}
}
}
}
}
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
});
List<Path> sourcePathList = getSourcePath(analyzer);
codeAnalyzer = new SourceCodeAnalyzer(analyzer, new SourceCodeResolver(sourcePathList));
for (Path sourcePath : sourcePathList) {
Files.walkFileTree(sourcePath, codeAnalyzer);
}
return false;
}
Expand All @@ -178,23 +69,10 @@ private List<Path> getSourcePath(Analyzer analyzer) {
}

public Clazz getPackageInfoClass(PackageRef packageRef) {
return packageInfoMap.get(packageRef);
}

private static boolean isType(String simpleOrFqdn, String type, List<?> imports) {
if (type.equals(simpleOrFqdn)) {
return true;
if (codeAnalyzer == null) {
return null;
}
if (type.endsWith("." + simpleOrFqdn)) {
for (Object object : imports) {
if (object instanceof ImportDeclaration importDecl) {
if (type.equals(importDecl.getName().getFullyQualifiedName())) {
return true;
}
}
}
}
return false;
return codeAnalyzer.packageInfoMap.get(packageRef);
}

}
Loading

0 comments on commit f301484

Please sign in to comment.