Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement experimental dependency analysis #63

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
<artifactId>missinglink-core</artifactId>
<packaging>jar</packaging>

<properties>
<version.shrinkwrap.resolvers>3.1.3</version.shrinkwrap.resolvers>
</properties>

<dependencies>
<dependency>
<groupId>javax.annotation</groupId>
Expand All @@ -22,11 +26,56 @@
<artifactId>asm-tree</artifactId>
<version>7.2</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>7.2</version>
</dependency>
<dependency>
<groupId>io.norberg</groupId>
<artifactId>auto-matter-annotation</artifactId>
<version>0.15.3</version>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap.resolver</groupId>
<artifactId>shrinkwrap-resolver-api</artifactId>
<version>${version.shrinkwrap.resolvers}</version>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap.resolver</groupId>
<artifactId>shrinkwrap-resolver-spi</artifactId>
<version>${version.shrinkwrap.resolvers}</version>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap.resolver</groupId>
<artifactId>shrinkwrap-resolver-api-maven</artifactId>
<version>${version.shrinkwrap.resolvers}</version>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap.resolver</groupId>
<artifactId>shrinkwrap-resolver-spi-maven</artifactId>
<version>${version.shrinkwrap.resolvers}</version>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap.resolver</groupId>
<artifactId>shrinkwrap-resolver-impl-maven</artifactId>
<version>${version.shrinkwrap.resolvers}</version>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap.resolver</groupId>
<artifactId>shrinkwrap-resolver-impl-maven-archive</artifactId>
<version>${version.shrinkwrap.resolvers}</version>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap.resolver</groupId>
<artifactId>shrinkwrap-resolver-api-maven-embedded</artifactId>
<version>${version.shrinkwrap.resolvers}</version>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap.resolver</groupId>
<artifactId>shrinkwrap-resolver-impl-maven-embedded</artifactId>
<version>${version.shrinkwrap.resolvers}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
Expand All @@ -39,6 +88,11 @@
<version>1.7.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20190722</version>
</dependency>
</dependencies>

<build>
Expand Down
38 changes: 23 additions & 15 deletions core/src/main/java/com/spotify/missinglink/ConflictChecker.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2015 Spotify AB
* Copyright (c) 2015-2019 Spotify AB
*
* 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
Expand Down Expand Up @@ -32,6 +32,7 @@
import com.spotify.missinglink.datamodel.MethodDependencyBuilder;
import com.spotify.missinglink.datamodel.MethodDescriptor;
import com.spotify.missinglink.datamodel.TypeDescriptor;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
Expand Down Expand Up @@ -95,14 +96,16 @@ public class ConflictChecker {
public static final ArtifactName UNKNOWN_ARTIFACT_NAME = new ArtifactName("<unknown>");

/**
*
* @param conflictFilter
* @param projectArtifact the main artifact of the project we're verifying
* (this is considered the entry point for reachability)
* @param artifactsToCheck all artifacts that are on the runtime classpath
* @param allArtifacts all artifacts, including implicit artifacts (runtime provided
* artifacts)
* @return a list of conflicts
*/
public List<Conflict> check(Artifact projectArtifact,
public List<Conflict> check(ConflictFilter conflictFilter, Artifact projectArtifact,
List<Artifact> artifactsToCheck,
List<Artifact> allArtifacts) {

Expand All @@ -126,8 +129,10 @@ public List<Conflict> check(Artifact projectArtifact,
}

for (DeclaredMethod method : clazz.methods().values()) {
conflicts.addAll(checkForBrokenMethodCalls(state, artifact, clazz, method));
conflicts.addAll(checkForBrokenFieldAccess(state, artifact, clazz, method));
conflicts.addAll(
checkForBrokenMethodCalls(state, artifact, clazz, method, conflictFilter));
conflicts.addAll(
checkForBrokenFieldAccess(state, artifact, clazz, method, conflictFilter));
}
}
}
Expand All @@ -154,7 +159,8 @@ private void createCanonicalClassMapping(CheckerStateBuilder stateBuilder,
private List<Conflict> checkForBrokenMethodCalls(CheckerState state,
Artifact artifact,
DeclaredClass clazz,
DeclaredMethod method) {
DeclaredMethod method,
ConflictFilter conflictFilter) {
List<Conflict> conflicts = new ArrayList<>();

for (CalledMethod calledMethod : method.methodCalls()) {
Expand All @@ -166,7 +172,7 @@ private List<Conflict> checkForBrokenMethodCalls(CheckerState state,
.caughtExceptions()
.stream()
.anyMatch(c -> c.getClassName().equals("java.lang.NoClassDefFoundError"));
if (!catchesNoClassDef) {
if (!catchesNoClassDef && conflictFilter.filterMissingClass(owningClass.getClassName())) {
conflicts.add(conflict(ConflictCategory.CLASS_NOT_FOUND,
"Class not found: " + owningClass,
dependency(clazz, method, calledMethod),
Expand Down Expand Up @@ -195,7 +201,8 @@ private List<Conflict> checkForBrokenMethodCalls(CheckerState state,

private List<Conflict> checkForBrokenFieldAccess(CheckerState state, Artifact artifact,
DeclaredClass clazz,
DeclaredMethod method) {
DeclaredMethod method,
ConflictFilter conflictFilter) {

List<Conflict> conflicts = new ArrayList<>();

Expand All @@ -209,12 +216,14 @@ private List<Conflict> checkForBrokenFieldAccess(CheckerState state, Artifact ar
.build();

if (calledClass == null) {
conflicts.add(conflict(ConflictCategory.CLASS_NOT_FOUND,
"Class not found: " + owningClass,
dependency(clazz, method, field),
artifact.name(),
state.sourceMappings().get(owningClass)
));
if (conflictFilter.filterMissingClass(owningClass.getClassName())) {
conflicts.add(conflict(ConflictCategory.CLASS_NOT_FOUND,
"Class not found: " + owningClass,
dependency(clazz, method, field),
artifact.name(),
state.sourceMappings().get(owningClass)
));
}
} else if (missingField(declaredField, calledClass, state.knownClasses())) {
conflicts.add(conflict(ConflictCategory.FIELD_NOT_FOUND,
"Field not found: " + field.name(),
Expand All @@ -231,7 +240,7 @@ private List<Conflict> checkForBrokenFieldAccess(CheckerState state, Artifact ar
return conflicts;
}

static Set<TypeDescriptor> reachableFrom(
Set<TypeDescriptor> reachableFrom(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, this change was unnecessary, will revert it!

Collection<DeclaredClass> values,
Map<ClassTypeDescriptor, DeclaredClass> knownClasses) {

Expand Down Expand Up @@ -369,5 +378,4 @@ private boolean missingField(DeclaredField field, DeclaredClass calledClass,

return true;
}

}
36 changes: 36 additions & 0 deletions core/src/main/java/com/spotify/missinglink/ConflictFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2019 Spotify AB
*
* 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 com.spotify.missinglink;

import java.util.Set;

public interface ConflictFilter {

/**
*
* @param className class to test
* @return true if a missing class should trigger a conflict, otherwise false
*/
boolean filterMissingClass(String className);

/**
*
* @param className class to test
* @return a set of artifacts that the class was expected to be in
*/
Set<String> getFoundIn(String className);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2019 Spotify AB
*
* 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 com.spotify.missinglink;

import java.util.Collections;
import java.util.Set;

public class EmptyConflictFilter implements ConflictFilter {
public static final ConflictFilter INSTANCE = new EmptyConflictFilter();

private EmptyConflictFilter() {
}

@Override
public boolean filterMissingClass(String className) {
return true;
}

@Override
public Set<String> getFoundIn(String className) {
return Collections.emptySet();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright (c) 2019 Spotify AB
*
* 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 com.spotify.missinglink.dependencies;

import org.json.JSONObject;
import org.json.JSONTokener;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import static java.lang.System.getProperty;

class ArtifactCache {
private final File dir;

private ArtifactCache(File dir) {
this.dir = dir;
if (!dir.exists()) {
if (!dir.mkdirs()) {
throw new RuntimeException("Could not create directory: " + dir);
}
}
if (!dir.isDirectory()) {
throw new RuntimeException("Expected a directory: " + dir);
}
}

static ArtifactCache getDefault() {
String homeDir = getProperty("user.home");
File root = new File(homeDir + "/.m2/repository/dependency-data");
return new ArtifactCache(root);
}

ArtifactContainer resolve(
Resolver resolver, Coordinate coordinate,
Callable<ArtifactContainer> fallback) {
if (coordinate.isSnapshot()) {
return invoke(fallback);
}
String filename = coordinate.toString().replace(':', '_') + ".json.gz";
File file = new File(dir, filename);
try {
if (file.exists()) {
JSONObject data = getObject(file);
IncompleteArtifact artifactContainer = JsonReader.fromJson(data);
Set<ArtifactContainer> dependencies = artifactContainer.getDependencies().stream()
.map(resolver::resolve)
.collect(Collectors.toSet());

return artifactContainer.complete(dependencies);
} else {
ArtifactContainer artifactContainer = invoke(fallback);
JSONObject jsonObject = JsonWriter.toJsonObject(artifactContainer);
writeObject(file, jsonObject);
return artifactContainer;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private ArtifactContainer invoke(Callable<ArtifactContainer> fallback) {
try {
return fallback.call();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
}

private void writeObject(File file, JSONObject object) throws IOException {
try (Writer writer = new OutputStreamWriter(new GZIPOutputStream(
new FileOutputStream(file)), StandardCharsets.UTF_8)) {
object.write(writer, 2, 0);
}
}

private JSONObject getObject(File file) throws IOException {
try (Reader reader = new InputStreamReader(new GZIPInputStream(
new FileInputStream(file)), StandardCharsets.UTF_8)) {
return new JSONObject(new JSONTokener(reader));
}
}
}
Loading