DslEnhancementReporter.java

/*******************************************************************************
 * Copyright (c) 2026 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.core.report;

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

import org.sandbox.mining.core.config.KnownRulesStore;
import org.sandbox.mining.core.config.KnownRulesStore.KnownRule;
import org.sandbox.mining.core.config.KnownRulesStore.RuleStatus;

/**
 * Generates reports from rules that require DSL enhancements.
 *
 * <p>Scans the {@link KnownRulesStore} for rules with status
 * {@link RuleStatus#NEEDS_DSL_EXTENSION} and groups them by the
 * DSL limitation they hit (inferred from category and summary).</p>
 *
 * <p>The generated report can be used to create GitHub issues
 * automatically from the mining workflow.</p>
 *
 * @since 1.3.3
 */
public class DslEnhancementReporter {

	/** A group of rules that share the same DSL limitation. */
	public static class DslLimitationGroup {
		private final String limitation;
		private final List<KnownRule> rules;

		DslLimitationGroup(String limitation, List<KnownRule> rules) {
			this.limitation = limitation;
			this.rules = List.copyOf(rules);
		}

		public String getLimitation() { return limitation; }
		public List<KnownRule> getRules() { return rules; }
		public int getCount() { return rules.size(); }
	}

	/**
	 * Scans the known rules store and groups rules that need DSL extensions
	 * by the DSL limitation they hit.
	 *
	 * @param store the known rules store to scan
	 * @return list of limitation groups, sorted by count (most impactful first)
	 */
	public List<DslLimitationGroup> groupByLimitation(KnownRulesStore store) {
		Map<String, List<KnownRule>> groups = new LinkedHashMap<>();
		for (KnownRule rule : store.getRules()) {
			if (rule.getStatus() != RuleStatus.NEEDS_DSL_EXTENSION) {
				continue;
			}
			String limitation = inferLimitation(rule);
			groups.computeIfAbsent(limitation, k -> new ArrayList<>()).add(rule);
		}
		return groups.entrySet().stream()
				.map(e -> new DslLimitationGroup(e.getKey(), e.getValue()))
				.sorted((a, b) -> Integer.compare(b.getCount(), a.getCount()))
				.toList();
	}

	/**
	 * Generates a structured markdown report of all DSL enhancement needs.
	 *
	 * @param store the known rules store to scan
	 * @return markdown report string, or empty string if no rules need DSL extensions
	 */
	public String generateReport(KnownRulesStore store) {
		List<DslLimitationGroup> groups = groupByLimitation(store);
		if (groups.isEmpty()) {
			return ""; //$NON-NLS-1$
		}

		StringBuilder sb = new StringBuilder();
		sb.append("# DSL Enhancement Needs\n\n"); //$NON-NLS-1$
		sb.append("Found ").append(groups.stream().mapToInt(DslLimitationGroup::getCount).sum()); //$NON-NLS-1$
		sb.append(" rules across ").append(groups.size()).append(" limitation categories.\n\n"); //$NON-NLS-1$ //$NON-NLS-2$

		for (DslLimitationGroup group : groups) {
			sb.append("## ").append(group.getLimitation()); //$NON-NLS-1$
			sb.append(" (").append(group.getCount()).append(" rules)\n\n"); //$NON-NLS-1$ //$NON-NLS-2$
			for (KnownRule rule : group.getRules()) {
				sb.append("- **").append(safe(rule.getSummary())).append("**"); //$NON-NLS-1$ //$NON-NLS-2$
				sb.append(" (category: ").append(safe(rule.getCategory())); //$NON-NLS-1$
				if (rule.getSourceCommit() != null) {
					sb.append(", commit: `").append(rule.getSourceCommit().substring(0, //$NON-NLS-1$
							Math.min(7, rule.getSourceCommit().length()))).append('`');
				}
				sb.append(")\n"); //$NON-NLS-1$
				if (rule.getDslRule() != null && !rule.getDslRule().isBlank()) {
					sb.append("  ```\n  ").append(rule.getDslRule().replace("\n", "\n  ")).append("\n  ```\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
				}
			}
			sb.append('\n');
		}
		return sb.toString();
	}

	/**
	 * Generates a list of issue descriptors, one per DSL limitation category.
	 * Each descriptor contains a title and body suitable for creating a GitHub issue.
	 *
	 * @param store the known rules store to scan
	 * @return list of issue descriptors
	 */
	public List<IssueDescriptor> generateIssueDescriptors(KnownRulesStore store) {
		List<DslLimitationGroup> groups = groupByLimitation(store);
		List<IssueDescriptor> issues = new ArrayList<>();
		for (DslLimitationGroup group : groups) {
			String title = "\uD83D\uDD27 DSL Enhancement: " + group.getLimitation() //$NON-NLS-1$
					+ " (" + group.getCount() + " rules blocked)"; //$NON-NLS-1$ //$NON-NLS-2$
			StringBuilder body = new StringBuilder();
			body.append("## DSL Limitation\n\n"); //$NON-NLS-1$
			body.append("**").append(group.getLimitation()).append("**\n\n"); //$NON-NLS-1$ //$NON-NLS-2$
			body.append("## Blocked Rules\n\n"); //$NON-NLS-1$
			body.append("| # | Summary | Category | Commit |\n"); //$NON-NLS-1$
			body.append("|---|---------|----------|--------|\n"); //$NON-NLS-1$
			int idx = 0;
			for (KnownRule rule : group.getRules()) {
				idx++;
				String shortCommit = rule.getSourceCommit() != null
						? rule.getSourceCommit().substring(0, Math.min(7, rule.getSourceCommit().length()))
						: "?"; //$NON-NLS-1$
				body.append("| ").append(idx).append(" | ").append(safe(rule.getSummary())); //$NON-NLS-1$ //$NON-NLS-2$
				body.append(" | ").append(safe(rule.getCategory())); //$NON-NLS-1$
				body.append(" | `").append(shortCommit).append("` |\n"); //$NON-NLS-1$ //$NON-NLS-2$
			}
			body.append("\n## Priority\n\n"); //$NON-NLS-1$
			body.append("This limitation blocks **").append(group.getCount()); //$NON-NLS-1$
			body.append("** discovered transformation rules from being implemented.\n"); //$NON-NLS-1$
			body.append("\n_Auto-generated by mining workflow._\n"); //$NON-NLS-1$
			issues.add(new IssueDescriptor(title, body.toString()));
		}
		return issues;
	}

	/**
	 * Descriptor for a GitHub issue to be created.
	 */
	public record IssueDescriptor(String title, String body) {
	}

	/**
	 * Infers the DSL limitation category from a rule's summary and category.
	 */
	static String inferLimitation(KnownRule rule) {
		String summary = rule.getSummary() != null ? rule.getSummary().toLowerCase() : ""; //$NON-NLS-1$
		String dslRule = rule.getDslRule() != null ? rule.getDslRule().toLowerCase() : ""; //$NON-NLS-1$

		if (summary.contains("bitwise") || dslRule.contains(" | ") || dslRule.contains("&")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			return "Bitwise operators in patterns/replacements"; //$NON-NLS-1$
		}
		if (summary.contains("try-with") || summary.contains("autocloseable") //$NON-NLS-1$ //$NON-NLS-2$
				|| summary.contains("resource wrapping")) { //$NON-NLS-1$
			return "Statement insertion / wrapping (try-with-resources, guard clauses)"; //$NON-NLS-1$
		}
		if (summary.contains("generic") || summary.contains("type parameter") //$NON-NLS-1$ //$NON-NLS-2$
				|| dslRule.contains("<$t>")) { //$NON-NLS-1$
			return "Type-parameterized matching (generics)"; //$NON-NLS-1$
		}
		if (summary.contains("arity") || summary.contains("vararg") //$NON-NLS-1$ //$NON-NLS-2$
				|| summary.contains("argument reorder")) { //$NON-NLS-1$
			return "Complex expression composition with arity changes"; //$NON-NLS-1$
		}
		if (summary.contains("control flow") || summary.contains("if-else") //$NON-NLS-1$ //$NON-NLS-2$
				|| summary.contains("multi-statement")) { //$NON-NLS-1$
			return "Control flow / multi-statement patterns"; //$NON-NLS-1$
		}
		return "Other DSL limitations"; //$NON-NLS-1$
	}

	private static String safe(String s) {
		return s != null ? s : "?"; //$NON-NLS-1$
	}
}