KotlinSMAP.java
/*******************************************************************************
* Copyright (c) 2009, 2026 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
* https://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Evgeny Mandrikov - initial API and implementation
*
*******************************************************************************/
package org.jacoco.core.internal.analysis.filter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Parsed representation of SourceDebugExtension attribute.
*/
public final class KotlinSMAP {
/**
* Parsed representation of a single LineSection from SourceDebugExtension
* attribute.
*/
public static final class Mapping {
private final String inputClassName;
private final int inputStartLine;
private final int repeatCount;
private final int outputStartLine;
/**
* Creates a new mapping.
*
* @param inputClassName
* name of input class
* @param inputStartLine
* starting line in input
* @param repeatCount
* number of mapped lines
* @param outputStartLine
* starting line in output
*/
Mapping(final String inputClassName, final int inputStartLine,
final int repeatCount, final int outputStartLine) {
this.inputClassName = inputClassName;
this.inputStartLine = inputStartLine;
this.repeatCount = repeatCount;
this.outputStartLine = outputStartLine;
}
/**
* @return name of input class
*/
public String inputClassName() {
return inputClassName;
}
/**
* @return starting line in input
*/
public int inputStartLine() {
return inputStartLine;
}
/**
* @return number of mapped lines
*/
public int repeatCount() {
return repeatCount;
}
/**
* @return starting line in output
*/
public int outputStartLine() {
return outputStartLine;
}
}
private final ArrayList<Mapping> mappings = new ArrayList<Mapping>();
/**
* Returns list of mappings.
*
* @return list of mappings
*/
public List<Mapping> mappings() {
return mappings;
}
/**
* Creates parsed representation of provided SourceDebugExtension attribute.
*
* @param sourceFileName
* the name of the source file from which the class with SMAP was
* compiled
* @param smap
* value of SourceDebugExtension attribute to parse
*/
public KotlinSMAP(final String sourceFileName, final String smap) {
try {
final BufferedReader br = new BufferedReader(
new StringReader(smap));
// Header
expectLine(br, "SMAP");
// OutputFileName
expectLine(br, sourceFileName);
// DefaultStratumId
expectLine(br, "Kotlin");
// StratumSection
expectLine(br, "*S Kotlin");
// FileSection
expectLine(br, "*F");
final HashMap<Integer, String> inputClassNames = new HashMap<Integer, String>();
String line;
while (!"*L".equals(line = br.readLine())) {
final Matcher m = FILE_INFO_PATTERN.matcher(line);
if (!m.matches()) {
throw new IllegalStateException(
"Unexpected SMAP line: " + line);
}
final int id = Integer.parseInt(m.group(1));
// See
// https://github.com/JetBrains/kotlin/blob/2.0.0/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/SMAP.kt#L120-L121
// https://github.com/JetBrains/kotlin/blob/2.0.0/compiler/backend/src/org/jetbrains/kotlin/codegen/SourceInfo.kt#L38-L41
final String className = br.readLine();
inputClassNames.put(id, className);
}
// LineSection
while (true) {
line = br.readLine();
if (line.equals("*E") || line.equals("*S KotlinDebug")) {
break;
}
final Matcher m = LINE_INFO_PATTERN.matcher(line);
if (!m.matches()) {
throw new IllegalStateException(
"Unexpected SMAP line: " + line);
}
final int inputStartLine = Integer.parseInt(m.group(1));
final int lineFileID = Integer
.parseInt(m.group(2).substring(1));
final String repeatCountOptional = m.group(3);
final int repeatCount = repeatCountOptional != null
? Integer.parseInt(repeatCountOptional.substring(1))
: 1;
final int outputStartLine = Integer.parseInt(m.group(4));
mappings.add(new Mapping(inputClassNames.get(lineFileID),
inputStartLine, repeatCount, outputStartLine));
}
} catch (final IOException e) {
// Must not happen with StringReader
throw new AssertionError(e);
}
}
private static void expectLine(final BufferedReader br,
final String expected) throws IOException {
final String line = br.readLine();
if (!expected.equals(line)) {
throw new IllegalStateException("Unexpected SMAP line: " + line);
}
}
private static final Pattern LINE_INFO_PATTERN = Pattern.compile("" //
+ "([0-9]++)" // InputStartLine
+ "(#[0-9]++)?+" // LineFileID
+ "(,[0-9]++)?+" // RepeatCount
+ ":([0-9]++)" // OutputStartLine
+ "(,[0-9]++)?+" // OutputLineIncrement
);
private static final Pattern FILE_INFO_PATTERN = Pattern.compile("" //
+ "\\+ ([0-9]++)" // FileID
+ " (.++)" // FileName
);
}