MiningReport.java

/*********************************************************************
 * Copyright (c) 2025 Carsten Hammer.
 *
 * 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:
 *     Carsten Hammer
 *************************************************************************/
package org.sandbox.mining.report;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Aggregates mining results from scanning repositories.
 */
public class MiningReport {

	/**
	 * A single match found during scanning.
	 */
	public record MatchEntry(String repoName, String hintFile, String ruleName, String filePath, int line,
								String matchedCode, String suggestedReplacement) {
	}

	private final List<MatchEntry> matches = new ArrayList<>();
	private final Map<String, Integer> fileCounts = new LinkedHashMap<>();
	private final Map<String, String> errors = new LinkedHashMap<>();

	/**
	 * Adds a match entry to the report.
	 */
	public void addMatch(String repoName, String hintFile, String ruleName, String filePath, int line,
							String matchedCode, String suggestedReplacement) {
		matches.add(new MatchEntry(repoName, hintFile, ruleName, filePath, line, matchedCode, suggestedReplacement));
	}

	/**
	 * Records the number of files scanned for a repository.
	 */
	public void addFileCount(String repoName, int count) {
		fileCounts.merge(repoName, count, Integer::sum);
	}

	/**
	 * Records an error that occurred while processing a repository.
	 */
	public void addError(String repoName, String errorMessage) {
		errors.put(repoName, errorMessage);
	}

	/**
	 * Merges another report into this one.
	 */
	public void merge(MiningReport other) {
		matches.addAll(other.matches);
		other.fileCounts.forEach((k, v) -> fileCounts.merge(k, v, Integer::sum));
		other.errors.forEach(errors::putIfAbsent);
	}

	public List<MatchEntry> getMatches() {
		return matches;
	}

	public Map<String, Integer> getFileCounts() {
		return fileCounts;
	}

	/**
	 * Returns errors recorded during scanning.
	 */
	public Map<String, String> getErrors() {
		return errors;
	}

	/**
	 * Returns true if any errors were recorded.
	 */
	public boolean hasErrors() {
		return !errors.isEmpty();
	}

	/**
	 * Returns matches grouped by repository name.
	 */
	public Map<String, List<MatchEntry>> getMatchesByRepo() {
		Map<String, List<MatchEntry>> byRepo = new LinkedHashMap<>();
		for (MatchEntry entry : matches) {
			byRepo.computeIfAbsent(entry.repoName(), k -> new ArrayList<>()).add(entry);
		}
		return byRepo;
	}

	/**
	 * Returns the number of distinct rules that matched for a given repository.
	 * Rules are distinguished by the combination of hint file and rule name,
	 * so that rules from different hint files are counted separately even if
	 * they share the same description.
	 */
	public long getDistinctRuleCount(String repoName) {
		return matches.stream().filter(m -> m.repoName().equals(repoName))
				.map(m -> m.hintFile() + "\0" + m.ruleName()).distinct().count();
	}

	/**
	 * Returns true if the report has any matches.
	 */
	public boolean hasMatches() {
		return !matches.isEmpty();
	}
}