Skip to content

Commit

Permalink
Diktat API (#1655)
Browse files Browse the repository at this point in the history
  • Loading branch information
nulls authored Apr 5, 2023
1 parent 08836e7 commit ef812c2
Show file tree
Hide file tree
Showing 71 changed files with 1,328 additions and 1,006 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/diktat_snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ jobs:
with:
gradle-version: wrapper
arguments: |
:diktat-api:publishToMavenLocal
:diktat-common:publishToMavenLocal
:diktat-ktlint-engine:publishToMavenLocal
:diktat-rules:publishToMavenLocal
:diktat-runner:diktat-runner-api:publishToMavenLocal
:diktat-runner:diktat-runner-ktlint-engine:publishToMavenLocal
:diktat-gradle-plugin:publishToMavenLocal
:generateLibsForDiktatSnapshot
-x detekt
Expand Down
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ tasks.create("generateLibsForDiktatSnapshot") {

val dependencies = setOf(
rootProject.project(":diktat-common"),
rootProject.project(":diktat-api"),
rootProject.project(":diktat-ktlint-engine"),
rootProject.project(":diktat-rules"),
rootProject.project(":diktat-runner:diktat-runner-api"),
rootProject.project(":diktat-runner:diktat-runner-ktlint-engine"),
rootProject.project(":diktat-gradle-plugin"),
)
mustRunAfter(dependencies.map { "${it.path}:publishToMavenLocal" })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ plugins {
id("org.cqfn.diktat.buildutils.publishing-signing-default-configuration")
}

project.description = "This module builds diktat-runner-api"
project.description = "This module builds diktat-api"

dependencies {
implementation(projects.diktatRules)
implementation(libs.kotlin.compiler.embeddable)
}
26 changes: 26 additions & 0 deletions diktat-api/src/main/kotlin/org/cqfn/diktat/DiktatProcessor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.cqfn.diktat

import org.cqfn.diktat.api.DiktatCallback
import java.nio.file.Path

/**
* Processor to run `diktat`
*/
interface DiktatProcessor {
/**
* Run `diktat fix` on provided [file] using [callback] for detected errors and returned formatted file content.
*
* @param file
* @param callback
* @return result of `diktat fix`
*/
fun fix(file: Path, callback: DiktatCallback): String

/**
* Run `diktat check` on provided [file] using [callback] for detected errors.
*
* @param file
* @param callback
*/
fun check(file: Path, callback: DiktatCallback)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.cqfn.diktat

import org.cqfn.diktat.api.DiktatRuleSet

/**
* A factory to create [DiktatProcessor] using [DiktatRuleSet]
*/
@FunctionalInterface
interface DiktatProcessorFactory : Function1<DiktatRuleSet, DiktatProcessor> {
/**
* @param diktatRuleSet
* @return created [DiktatProcessor] using [DiktatRuleSet]
*/
override operator fun invoke(diktatRuleSet: DiktatRuleSet): DiktatProcessor
}
95 changes: 95 additions & 0 deletions diktat-api/src/main/kotlin/org/cqfn/diktat/DiktatRunner.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package org.cqfn.diktat

import org.cqfn.diktat.api.DiktatBaseline
import org.cqfn.diktat.api.DiktatBaseline.Companion.skipKnownErrors
import org.cqfn.diktat.api.DiktatProcessorListener
import org.cqfn.diktat.api.DiktatProcessorListener.Companion.countErrorsAsProcessorListener
import org.cqfn.diktat.api.DiktatReporter
import java.nio.file.Path
import java.util.concurrent.atomic.AtomicInteger
import kotlin.io.path.readText
import kotlin.io.path.writeText

private typealias RunAction = (DiktatProcessor, DiktatProcessorListener) -> Unit

/**
* A runner for diktat on bunch of files using baseline and reporter
*
* @property diktatProcessor
* @property diktatBaseline
* @property diktatBaselineGenerator
* @property diktatReporter
* @property diktatReporterCloser
*/
data class DiktatRunner(
val diktatProcessor: DiktatProcessor,
val diktatBaseline: DiktatBaseline,
private val diktatBaselineGenerator: DiktatProcessorListener,
val diktatReporter: DiktatReporter,
private val diktatReporterCloser: DiktatProcessorListener,
) {
private fun doRun(
args: DiktatRunnerArguments,
runAction: RunAction,
): Int {
val errorCounter = AtomicInteger()
runAction(
diktatProcessor,
DiktatProcessorListener(
args.loggingListener,
diktatReporter.skipKnownErrors(diktatBaseline),
diktatReporterCloser,
diktatBaselineGenerator,
errorCounter.countErrorsAsProcessorListener()
),
)
return errorCounter.get()
}

/**
* Run `diktat fix` for all [files].
*
* @param args
* @param fileUpdateNotifier notifier about updated files
* @return count of detected errors
*/
fun fixAll(
args: DiktatRunnerArguments,
fileUpdateNotifier: (Path) -> Unit,
): Int = doRun(args) { processor, listener ->
listener.beforeAll(args.files)
args.files.forEach { file ->
listener.before(file)
val formattedContent = processor.fix(file) { error, isCorrected ->
listener.onError(file, error, isCorrected)
}
val fileContent = file.readText(Charsets.UTF_8)
if (fileContent != formattedContent) {
fileUpdateNotifier(file)
file.writeText(formattedContent, Charsets.UTF_8)
}
listener.after(file)
}
listener.afterAll()
}

/**
* Run `diktat check` for all [files].
*
* @param args
* @return count of detected errors
*/
fun checkAll(
args: DiktatRunnerArguments,
): Int = doRun(args) { processor, listener ->
listener.beforeAll(args.files)
args.files.forEach { file ->
listener.before(file)
processor.check(file) { error, isCorrected ->
listener.onError(file, error, isCorrected)
}
listener.after(file)
}
listener.afterAll()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.cqfn.diktat

import org.cqfn.diktat.api.DiktatProcessorListener
import java.io.OutputStream
import java.nio.file.Path
import kotlin.io.path.absolutePathString

/**
* Arguments for [DiktatRunner]
*
* @property configFileName a config file to load Diktat's rules
* @property sourceRootDir a common root dir for all provided [files]
* @property files a collection of files which needs to be fixed
* @property baselineFile an optional path to file with baseline
* @property reporterType type of reporter to report the detected errors
* @property reporterOutput output for reporter
* @property loggingListener listener to log diktat runner phases
*/
data class DiktatRunnerArguments(
val configFileName: String,
val sourceRootDir: Path,
val files: Collection<Path>,
val baselineFile: Path?,
val reporterType: String,
val reporterOutput: OutputStream?,
val loggingListener: DiktatProcessorListener = DiktatProcessorListener.empty,
) {
constructor(
configFile: Path,
sourceRootDir: Path,
files: Collection<Path>,
baselineFile: Path?,
reporterType: String,
reporterOutput: OutputStream?,
loggingListener: DiktatProcessorListener,
) : this(
configFile.absolutePathString(),
sourceRootDir,
files,
baselineFile,
reporterType,
reporterOutput,
loggingListener,
)
}
66 changes: 66 additions & 0 deletions diktat-api/src/main/kotlin/org/cqfn/diktat/DiktatRunnerFactory.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package org.cqfn.diktat

import org.cqfn.diktat.api.DiktatBaseline
import org.cqfn.diktat.api.DiktatBaselineFactory
import org.cqfn.diktat.api.DiktatProcessorListener
import org.cqfn.diktat.api.DiktatProcessorListener.Companion.closeAfterAllAsProcessorListener
import org.cqfn.diktat.api.DiktatReporter
import org.cqfn.diktat.api.DiktatReporterFactory
import org.cqfn.diktat.api.DiktatRuleSetFactory
import java.io.OutputStream
import java.nio.file.Path

/**
* A factory to create [DiktatRunner]
*/
class DiktatRunnerFactory(
private val diktatRuleSetFactory: DiktatRuleSetFactory,
private val diktatProcessorFactory: DiktatProcessorFactory,
private val diktatBaselineFactory: DiktatBaselineFactory,
private val diktatReporterFactory: DiktatReporterFactory,
) : Function1<DiktatRunnerArguments, DiktatRunner> {
/**
* @param args
* @return an instance of [DiktatRunner] created using [args]
*/
override fun invoke(args: DiktatRunnerArguments): DiktatRunner {
val diktatRuleSet = diktatRuleSetFactory.create(args.configFileName)
val processor = diktatProcessorFactory(diktatRuleSet)
val (baseline, baselineGenerator) = resolveBaseline(args.baselineFile, args.sourceRootDir)
val (reporter, closer) = resolveReporter(args.reporterType, args.reporterOutput, args.sourceRootDir)
return DiktatRunner(
diktatProcessor = processor,
diktatBaseline = baseline,
diktatBaselineGenerator = baselineGenerator,
diktatReporter = reporter,
diktatReporterCloser = closer,
)
}

private fun resolveBaseline(
baselineFile: Path?,
sourceRootDir: Path,
): Pair<DiktatBaseline, DiktatProcessorListener> = baselineFile
?.let { diktatBaselineFactory.tryToLoad(it, sourceRootDir) }
?.let { it to DiktatProcessorListener.empty }
?: run {
val baselineGenerator = baselineFile?.let {
diktatBaselineFactory.generator(it, sourceRootDir)
} ?: DiktatProcessorListener.empty
DiktatBaseline.empty to baselineGenerator
}

private fun resolveReporter(
reporterType: String,
reporterOutput: OutputStream?,
sourceRootDir: Path,
): Pair<DiktatReporter, DiktatProcessorListener> {
val (outputStream, closeListener) = reporterOutput
?.let { it to it.closeAfterAllAsProcessorListener() }
?: run {
System.`out` to DiktatProcessorListener.empty
}
val actualReporter = diktatReporterFactory(reporterType, outputStream, sourceRootDir)
return actualReporter to closeListener
}
}
42 changes: 42 additions & 0 deletions diktat-api/src/main/kotlin/org/cqfn/diktat/api/DiktatBaseline.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.cqfn.diktat.api

import java.nio.file.Path

/**
* A base interface for Baseline
*/
fun interface DiktatBaseline {
/**
* @param file
* @return a set of [DiktatError] found in baseline by [file]
*/
fun errorsByFile(file: Path): Set<DiktatError>

companion object {
/**
* Empty [DiktatBaseline]
*/
val empty: DiktatBaseline = DiktatBaseline { _ -> emptySet() }

/**
* @param baseline
* @return wrapped [DiktatProcessorListener] which skips known errors based on [baseline]
*/
fun DiktatProcessorListener.skipKnownErrors(baseline: DiktatBaseline): DiktatProcessorListener = object : DiktatProcessorListener {
override fun onError(
file: Path,
error: DiktatError,
isCorrected: Boolean
) {
if (!baseline.errorsByFile(file).contains(error)) {
this@skipKnownErrors.onError(file, error, isCorrected)
}
}

override fun beforeAll(files: Collection<Path>) = this@skipKnownErrors.beforeAll(files)
override fun before(file: Path) = this@skipKnownErrors.before(file)
override fun after(file: Path) = this@skipKnownErrors.after(file)
override fun afterAll() = this@skipKnownErrors.afterAll()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.cqfn.diktat.api

import java.nio.file.Path

/**
* A factory to load or generate [DiktatBaseline]
*/
interface DiktatBaselineFactory {
/**
* @param baselineFile
* @param sourceRootDir a dir to detect relative path for processing files
* @return Loaded [DiktatBaseline] from [baselineFile] or null if it gets an error in loading
*/
fun tryToLoad(
baselineFile: Path,
sourceRootDir: Path,
): DiktatBaseline?

/**
* @param baselineFile
* @param sourceRootDir a dir to detect relative path for processing files
* @return [DiktatProcessorListener] which generates baseline in [baselineFile]
*/
fun generator(
baselineFile: Path,
sourceRootDir: Path,
): DiktatProcessorListener
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,11 @@ fun interface DiktatCallback : Function2<DiktatError, Boolean, Unit> {
* @param isCorrected true if the error fixed by diktat
*/
override fun invoke(error: DiktatError, isCorrected: Boolean)

companion object {
/**
* [DiktatCallback] that does nothing
*/
val empty: DiktatCallback = DiktatCallback { _, _ -> }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.cqfn.diktat.api

/**
* The **file-specific** error emitter, initialized and used in [DiktatRule] implementations.
*
* Since the file is indirectly a part of the state of a `DiktatRule`, the same
* `DiktatRule` instance should **never be re-used** to check more than a single
* file, or confusing effects (incl. race conditions) will occur.
*
* @see DiktatRule
*/
fun interface DiktatErrorEmitter : Function3<Int, String, Boolean, Unit> {
/**
* @param offset
* @param errorMessage
* @param canBeAutoCorrected
*/
override fun invoke(
offset: Int,
errorMessage: String,
canBeAutoCorrected: Boolean
)
}
Loading

0 comments on commit ef812c2

Please sign in to comment.