Analyzer.java
/*******************************************************************************
* Copyright (c) 2009, 2024 Mountainminds GmbH & Co. KG and Contributors
* This program and the accompanying materials are made available under
* the terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Marc R. Hoffmann - initial API and implementation
*
*******************************************************************************/
package org.jacoco.core.analysis;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.StringTokenizer;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.jacoco.core.JaCoCo;
import org.jacoco.core.data.ExecutionData;
import org.jacoco.core.data.ExecutionDataStore;
import org.jacoco.core.internal.ContentTypeDetector;
import org.jacoco.core.internal.InputStreams;
import org.jacoco.core.internal.Pack200Streams;
import org.jacoco.core.internal.analysis.ClassAnalyzer;
import org.jacoco.core.internal.analysis.ClassCoverageImpl;
import org.jacoco.core.internal.analysis.StringPool;
import org.jacoco.core.internal.data.CRC64;
import org.jacoco.core.internal.flow.ClassProbesAdapter;
import org.jacoco.core.internal.instr.InstrSupport;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;
/**
* An {@link Analyzer} instance processes a set of Java class files and
* calculates coverage data for them. For each class file the result is reported
* to a given {@link ICoverageVisitor} instance. In addition the
* {@link Analyzer} requires a {@link ExecutionDataStore} instance that holds
* the execution data for the classes to analyze. The {@link Analyzer} offers
* several methods to analyze classes from a variety of sources.
*/
public class Analyzer {
private final ExecutionDataStore executionData;
private final ICoverageVisitor coverageVisitor;
private final StringPool stringPool;
/**
* Creates a new analyzer reporting to the given output.
*
* @param executionData
* execution data
* @param coverageVisitor
* the output instance that will coverage data for every analyzed
* class
*/
public Analyzer(final ExecutionDataStore executionData,
final ICoverageVisitor coverageVisitor) {
this.executionData = executionData;
this.coverageVisitor = coverageVisitor;
this.stringPool = new StringPool();
}
/**
* Creates an ASM class visitor for analysis.
*
* @param classid
* id of the class calculated with {@link CRC64}
* @param className
* VM name of the class
* @return ASM visitor to write class definition to
*/
private ClassVisitor createAnalyzingVisitor(final long classid,
final String className) {
final ExecutionData data = executionData.get(classid);
final boolean[] probes;
final boolean noMatch;
if (data == null) {
probes = null;
noMatch = executionData.contains(className);
} else {
probes = data.getProbes();
noMatch = false;
}
final ClassCoverageImpl coverage = new ClassCoverageImpl(className,
classid, noMatch);
final ClassAnalyzer analyzer = new ClassAnalyzer(coverage, probes,
stringPool) {
@Override
public void visitEnd() {
super.visitEnd();
coverageVisitor.visitCoverage(coverage);
}
};
return new ClassProbesAdapter(analyzer, false);
}
private void analyzeClass(final byte[] source) {
final long classId = CRC64.classId(source);
final ClassReader reader = InstrSupport.classReaderFor(source);
if ((reader.getAccess() & Opcodes.ACC_MODULE) != 0) {
return;
}
if ((reader.getAccess() & Opcodes.ACC_SYNTHETIC) != 0) {
return;
}
final ClassVisitor visitor = createAnalyzingVisitor(classId,
reader.getClassName());
reader.accept(visitor, 0);
}
/**
* Analyzes the class definition from a given in-memory buffer.
*
* @param buffer
* class definitions
* @param location
* a location description used for exception messages
* @throws IOException
* if the class can't be analyzed
*/
public void analyzeClass(final byte[] buffer, final String location)
throws IOException {
try {
analyzeClass(buffer);
} catch (final RuntimeException cause) {
throw analyzerError(location, cause);
}
}
/**
* Analyzes the class definition from a given input stream. The provided
* {@link InputStream} is not closed by this method.
*
* @param input
* stream to read class definition from
* @param location
* a location description used for exception messages
* @throws IOException
* if the stream can't be read or the class can't be analyzed
*/
public void analyzeClass(final InputStream input, final String location)
throws IOException {
final byte[] buffer;
try {
buffer = InputStreams.readFully(input);
} catch (final IOException e) {
throw analyzerError(location, e);
}
analyzeClass(buffer, location);
}
private IOException analyzerError(final String location,
final Exception cause) {
final IOException ex = new IOException(
String.format("Error while analyzing %s with JaCoCo %s/%s.",
location, JaCoCo.VERSION, JaCoCo.COMMITID_SHORT));
ex.initCause(cause);
return ex;
}
/**
* Analyzes all classes found in the given input stream. The input stream
* may either represent a single class file, a ZIP archive, a Pack200
* archive or a gzip stream that is searched recursively for class files.
* All other content types are ignored. The provided {@link InputStream} is
* not closed by this method.
*
* @param input
* input data
* @param location
* a location description used for exception messages
* @return number of class files found
* @throws IOException
* if the stream can't be read or a class can't be analyzed
*/
public int analyzeAll(final InputStream input, final String location)
throws IOException {
final ContentTypeDetector detector;
try {
detector = new ContentTypeDetector(input);
} catch (final IOException e) {
throw analyzerError(location, e);
}
switch (detector.getType()) {
case ContentTypeDetector.CLASSFILE:
analyzeClass(detector.getInputStream(), location);
return 1;
case ContentTypeDetector.ZIPFILE:
return analyzeZip(detector.getInputStream(), location);
case ContentTypeDetector.GZFILE:
return analyzeGzip(detector.getInputStream(), location);
case ContentTypeDetector.PACK200FILE:
return analyzePack200(detector.getInputStream(), location);
default:
return 0;
}
}
/**
* Analyzes all class files contained in the given file or folder. Class
* files as well as ZIP files are considered. Folders are searched
* recursively.
*
* @param file
* file or folder to look for class files
* @return number of class files found
* @throws IOException
* if the file can't be read or a class can't be analyzed
*/
public int analyzeAll(final File file) throws IOException {
int count = 0;
if (file.isDirectory()) {
for (final File f : file.listFiles()) {
count += analyzeAll(f);
}
} else {
final InputStream in = new FileInputStream(file);
try {
count += analyzeAll(in, file.getPath());
} finally {
in.close();
}
}
return count;
}
/**
* Analyzes all classes from the given class path. Directories containing
* class files as well as archive files are considered.
*
* @param path
* path definition
* @param basedir
* optional base directory, if <code>null</code> the current
* working directory is used as the base for relative path
* entries
* @return number of class files found
* @throws IOException
* if a file can't be read or a class can't be analyzed
*/
public int analyzeAll(final String path, final File basedir)
throws IOException {
int count = 0;
final StringTokenizer st = new StringTokenizer(path,
File.pathSeparator);
while (st.hasMoreTokens()) {
count += analyzeAll(new File(basedir, st.nextToken()));
}
return count;
}
private int analyzeZip(final InputStream input, final String location)
throws IOException {
final ZipInputStream zip = new ZipInputStream(input);
ZipEntry entry;
int count = 0;
while ((entry = nextEntry(zip, location)) != null) {
count += analyzeAll(zip, location + "@" + entry.getName());
}
return count;
}
private ZipEntry nextEntry(final ZipInputStream input,
final String location) throws IOException {
try {
return input.getNextEntry();
} catch (final IOException e) {
throw analyzerError(location, e);
}
}
private int analyzeGzip(final InputStream input, final String location)
throws IOException {
GZIPInputStream gzipInputStream;
try {
gzipInputStream = new GZIPInputStream(input);
} catch (final IOException e) {
throw analyzerError(location, e);
}
return analyzeAll(gzipInputStream, location);
}
private int analyzePack200(final InputStream input, final String location)
throws IOException {
InputStream unpackedInput;
try {
unpackedInput = Pack200Streams.unpack(input);
} catch (final IOException e) {
throw analyzerError(location, e);
}
return analyzeAll(unpackedInput, location);
}
}