All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.twisterrob.gradle.quality.report.html.ViolationsDeduplicator.kt Maven / Gradle / Ivy

The newest version!
@file:Suppress("detekt.TooManyFunctions") // This defines a whole module in one file.

package net.twisterrob.gradle.quality.report.html

import net.twisterrob.gradle.common.ALL_VARIANTS_NAME
import net.twisterrob.gradle.quality.Violation
import net.twisterrob.gradle.quality.Violations
import java.io.File

private typealias Module = String
private typealias Variant = String
private typealias Parser = String

/**
 * Remove duplicate violations.
 * Duplicates are: in the same location and the same problem, see [Deduper].
 * Approach: get violations that affect all variants and remove them from variant-specific violations
 */
fun deduplicate(violations: List): List {
	@Suppress("USELESS_CAST") // Make sure chains use the typealias.
	return violations
		.groupBy { it.module as Module }
		.mapValues { (_, list) -> process(mergeIntersections(list)) }
		.values
		.flatten()
}

private fun process(violations: List): List {
	@Suppress("USELESS_CAST") // Make sure chains use the typealias.
	val byVariant = violations.groupBy { it.variant as Variant }
	val all = byVariant[ALL_VARIANTS_NAME] ?: return violations
	val filtered = byVariant.filterKeys { it != ALL_VARIANTS_NAME }
	val deduplicated = filtered.flatMap { (_, violations) ->
		violations.map { removeDuplicates(from = it, using = all) }
	}
	return all + deduplicated
}

private fun removeDuplicates(from: Violations, using: List): Violations =
	using.fold(from) { reduced, next -> removeDuplicates(reduced, next) }

private fun removeDuplicates(from: Violations, using: Violations): Violations {
	return Violations(
		parser = from.parser,
		module = from.module,
		variant = from.variant,
		result = from.result,
		report = from.report,
		violations = removeOptionalDuplicates(from.violations, using.violations)
	)
}

private fun removeOptionalDuplicates(from: List?, using: List?): List? {
	if (from == null) return null // Nothing to remove from, identity.
	if (using == null) return from // No duplicates to remove, keep everything.
	return removeDuplicates(from, using)
}

@Suppress("ConvertArgumentToSet")
private fun removeDuplicates(from: List, using: List): List {
	val set = from.map { Deduper(it) }.toMutableSet()
	set.removeAll(using.map { Deduper(it) })
	return set.map { it.violation }
}

/**
 * This step will duplicate some violations in variant and "all", but expecting [process] to remove those.
 */
private fun mergeIntersections(violations: List): List =
	violations
		.groupBy { it.parser.rewrite() }
		.flatMap { (_, list) -> mergeIntersectionsForParser(list) }

@Suppress("detekt.ReturnCount") // Open to suggestions.
private fun mergeIntersectionsForParser(violations: List): List {
	@Suppress("USELESS_CAST") // Make sure chains use the typealiases.
	val byVariant = violations.groupBy { it.variant as Variant }
	val filtered = byVariant.filterKeys { it != ALL_VARIANTS_NAME }
	if (filtered.size < 2) {
		// Only one variant, don't even try to introduce "all" variants. Also, might be no variants at all.
		return violations
	}
	val intersection = filtered.values.map { it.violations }.intersect()
	if (intersection.isEmpty()) {
		// No common problems, leave everything as it was.
		return violations
	}
	val all = byVariant[ALL_VARIANTS_NAME]?.singleOrNull()
	if (all != null) {
		val newAll = Violations(
			parser = all.parser,
			module = all.module,
			variant = all.variant,
			result = all.result,
			report = all.report,
			violations = all.violations + removeOptionalDuplicates(intersection, all.violations),
		)
		return listOf(newAll) + (violations - all)
	} else {
		// Create new * variant with all the common violations.
		val representative = violations.first()
		val newAll = Violations(
			parser = representative.parser.rewrite(),
			module = representative.module,
			variant = ALL_VARIANTS_NAME,
			result = File("."),
			report = File("."),
			violations = intersection,
		)
		return listOf(newAll) + violations
	}
}

private val Iterable.violations: List
	get() = this.flatMap { it.violations.orEmpty() }

private fun Iterable>.intersect(): List =
	this.reduce { acc, next -> intersect(acc, next) }

@Suppress("ConvertArgumentToSet")
private fun intersect(list1: List, list2: List): List {
	val list1D = list1.map { Deduper(it) }
	val list2D = list2.map { Deduper(it) }
	val intersection = list1D intersect list2D
	return intersection.map { it.violation }
}

@Suppress("detekt.UseDataClass") // External equals/hashCode for deduplication.
private class Deduper(val violation: Violation) {

	override fun equals(other: Any?): Boolean {
		if (this === other) return true
		if (other !is Deduper) return false

		if (violation.rule != other.violation.rule) return false
		if (violation.category != other.violation.category) return false
		if (violation.message != other.violation.message) return false
		if (violation.location.module != other.violation.location.module) return false
		if (violation.location.file != other.violation.location.file) return false
		if (violation.location.startLine != other.violation.location.startLine) return false
		if (violation.location.endLine != other.violation.location.endLine) return false
		if (violation.location.column != other.violation.location.column) return false

		return true
	}

	override fun hashCode(): Int {
		var result = violation.rule.hashCode()
		result = 31 * result + violation.message.hashCode()
		result = 31 * result + (violation.category?.hashCode() ?: 0)
		result = 31 * result + violation.location.module.hashCode()
		result = 31 * result + violation.location.file.hashCode()
		result = 31 * result + violation.location.startLine.hashCode()
		result = 31 * result + violation.location.endLine.hashCode()
		result = 31 * result + violation.location.column.hashCode()
		return result
	}
}

private fun Parser.rewrite(): Parser =
	if (this == "lintVariant") "lint" else this

/**
 * Required bridge to mirror [kotlin.collections.plus] because overload resolution would match the nullable one.
 */
@JvmName("plusOriginal")
private operator fun  List.plus(other: List): List =
	(this as Collection).plus(other)

private operator fun  List?.plus(other: List?): List? =
	when {
		this == null && other == null -> null
		this == null -> other
		other == null -> this
		else -> this + other
	}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy