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.
  • Loading branch information
laeubi committed Feb 13, 2025
1 parent 9eb105a commit 2ee7d0d
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 2ee7d0d

Please sign in to comment.