Compare commits
12 Commits
master
...
feature/in
| Author | SHA1 | Date |
|---|---|---|
|
|
3d4ada6bbf | |
|
|
a0c62581de | |
|
|
2ebdc7e33f | |
|
|
ab6a83aee0 | |
|
|
6df29f2101 | |
|
|
dca38422f3 | |
|
|
5c46386ebf | |
|
|
3cc63ffe5e | |
|
|
ec358728eb | |
|
|
78024ea1a6 | |
|
|
5e7a39d14d | |
|
|
e175e66e57 |
|
|
@ -13,7 +13,7 @@ repositories {
|
|||
|
||||
dependencies {
|
||||
// android gradle plugin, required by custom plugin
|
||||
implementation("com.android.tools.build:gradle:4.0.1")
|
||||
implementation("com.android.tools.build:gradle:7.1.3")
|
||||
|
||||
implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.10.0")
|
||||
implementation("de.aaschmid:gradle-cpd-plugin:3.1")
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import org.gradle.api.Project
|
|||
import org.gradle.api.tasks.compile.JavaCompile
|
||||
import org.gradle.kotlin.dsl.findByType
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import static_analysis.utils.getApiGeneratorExtension
|
||||
|
||||
class ApiGeneratorAndroidPlugin : ApiGeneratorPlugin() {
|
||||
|
||||
|
|
@ -13,11 +14,10 @@ class ApiGeneratorAndroidPlugin : ApiGeneratorPlugin() {
|
|||
super.apply(target)
|
||||
|
||||
with(target) {
|
||||
val extension = getExtension()
|
||||
val extension = getApiGeneratorExtension()
|
||||
val outputDir = getDirectoryForGeneration()
|
||||
|
||||
extension.outputDirPath = outputDir.path
|
||||
extension.recreateOutputDir = true
|
||||
|
||||
afterEvaluate {
|
||||
extensions.findByType<LibraryExtension>()?.apply {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
package apigen
|
||||
|
||||
import org.gradle.api.Project
|
||||
import static_analysis.utils.getApiGeneratorExtension
|
||||
|
||||
class ApiGeneratorBackendPlugin : ApiGeneratorPlugin() {
|
||||
|
||||
override fun apply(target: Project) {
|
||||
super.apply(target)
|
||||
|
||||
val extension = target.getExtension()
|
||||
val extension = target.getApiGeneratorExtension()
|
||||
|
||||
extension.outputDirPath = target.file("src/main/kotlin").path
|
||||
extension.recreateOutputDir = false
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import org.gradle.api.Project
|
|||
import org.gradle.api.Task
|
||||
import org.gradle.kotlin.dsl.create
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
import org.gradle.kotlin.dsl.register
|
||||
import org.gradle.kotlin.dsl.repositories
|
||||
|
||||
abstract class ApiGeneratorPlugin : Plugin<Project> {
|
||||
|
|
@ -13,6 +14,8 @@ abstract class ApiGeneratorPlugin : Plugin<Project> {
|
|||
const val API_GENERATOR_CONFIG = "apiGenerator"
|
||||
const val API_GENERATOR_EXT_NAME = "apiGenerator"
|
||||
const val API_GENERATOR_DEFAULT_VERSION = "1.4.0-beta10"
|
||||
|
||||
const val API_GENERATOR_CLEAN_TEMP_DIR_TASK = "apiGeneratorCleanTempDir"
|
||||
}
|
||||
|
||||
override fun apply(target: Project) {
|
||||
|
|
@ -34,7 +37,8 @@ abstract class ApiGeneratorPlugin : Plugin<Project> {
|
|||
|
||||
extensions.create<ApiGeneratorExtension>(API_GENERATOR_EXT_NAME)
|
||||
|
||||
val apiGenTask = createApiGeneratorTask()
|
||||
val cleanTask = tasks.create(API_GENERATOR_CLEAN_TEMP_DIR_TASK)
|
||||
val apiGenTask = getApiGenTask(cleanTask)
|
||||
|
||||
gradle.projectsEvaluated {
|
||||
tasks.getByName("preBuild").dependsOn(apiGenTask)
|
||||
|
|
@ -42,35 +46,15 @@ abstract class ApiGeneratorPlugin : Plugin<Project> {
|
|||
}
|
||||
}
|
||||
|
||||
protected fun Project.getExtension(): ApiGeneratorExtension = extensions.getByName(API_GENERATOR_EXT_NAME) as ApiGeneratorExtension
|
||||
private fun Project.getApiGenTask(cleanTask: Task) = tasks.register(API_GENERATOR_CONFIG, ApiGeneratorTask::class) {
|
||||
val apiGeneratorTempDir = temporaryDir
|
||||
tempApiDirDirectory.set(apiGeneratorTempDir.absolutePath)
|
||||
|
||||
private fun Project.createApiGeneratorTask(): Task = tasks.create(API_GENERATOR_CONFIG).doLast {
|
||||
|
||||
val extension = getExtension()
|
||||
|
||||
val pathToApiSchemes = extension.pathToApiSchemes ?: throw IllegalStateException("Configure path to api schemes for api generator plugin")
|
||||
val outputLanguage = extension.outputLanguage ?: throw IllegalStateException("Configure output language code for api generator plugin")
|
||||
|
||||
javaexec {
|
||||
main = "-jar"
|
||||
workingDir = rootDir
|
||||
args = listOfNotNull(
|
||||
configurations.getByName("apiGenerator").asPath,
|
||||
"generate-client-code",
|
||||
"--output-language",
|
||||
outputLanguage.argName,
|
||||
"--specification-path",
|
||||
pathToApiSchemes,
|
||||
"--kotlin-methods-generation-mode".takeIf { outputLanguage.methodOutputType != null },
|
||||
outputLanguage.methodOutputType?.argName,
|
||||
"--output-path",
|
||||
extension.outputDirPath,
|
||||
"--package-name",
|
||||
extension.outputPackageName,
|
||||
"--recreate_output_dirs",
|
||||
extension.recreateOutputDir.toString()
|
||||
)
|
||||
}
|
||||
// Нужно для удаления временной директории даже в случае краша во время выполнения таски.
|
||||
finalizedBy(
|
||||
cleanTask.doFirst {
|
||||
apiGeneratorTempDir.deleteRecursively()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
package apigen
|
||||
|
||||
import apigen.depencency_resolver.ApiModelsDependencyResolver
|
||||
import apigen.depencency_resolver.CopyApiModelsToTempDirTask
|
||||
import apigen.depencency_resolver.FileParser
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.FileTree
|
||||
import org.gradle.api.file.FileType
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.InputFiles
|
||||
import org.gradle.api.tasks.Internal
|
||||
import org.gradle.api.tasks.OutputFiles
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import org.gradle.work.ChangeType
|
||||
import org.gradle.work.Incremental
|
||||
import org.gradle.work.InputChanges
|
||||
import org.gradle.workers.WorkerExecutor
|
||||
import static_analysis.utils.getApiGeneratorExtension
|
||||
import static_analysis.utils.toPath
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
abstract class ApiGeneratorTask: DefaultTask() {
|
||||
|
||||
@Internal
|
||||
val extension: ApiGeneratorExtension = project.getApiGeneratorExtension()
|
||||
|
||||
@Internal
|
||||
val pathToApiScheme: String = extension.pathToApiSchemes
|
||||
?: throw IllegalStateException("Configure path to api schemes for api generator plugin")
|
||||
|
||||
@Internal
|
||||
val outputLanguage: OutputLanguage = extension.outputLanguage
|
||||
?: throw IllegalStateException("Configure output language code for api generator plugin")
|
||||
|
||||
@Inject
|
||||
abstract fun getWorkerExecutor(): WorkerExecutor
|
||||
|
||||
@Incremental
|
||||
@InputFiles
|
||||
val inputFiles: FileTree = project.files(pathToApiScheme).asFileTree
|
||||
|
||||
@OutputFiles
|
||||
val outputFiles: FileTree = project.files(extension.outputDirPath).asFileTree
|
||||
|
||||
@get:Input
|
||||
abstract val tempApiDirDirectory: Property<String>
|
||||
|
||||
@TaskAction
|
||||
fun execute(inputChanges: InputChanges): Unit = with(project) {
|
||||
if (inputChanges.isIncremental) {
|
||||
handleInputChanges(inputChanges)
|
||||
} else {
|
||||
generateApi(
|
||||
pathToApi = pathToApiScheme,
|
||||
outputLanguage = outputLanguage,
|
||||
recreateOutputDir = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateApi(
|
||||
pathToApi: String,
|
||||
outputLanguage: OutputLanguage,
|
||||
recreateOutputDir: Boolean
|
||||
) = with(project) {
|
||||
javaexec {
|
||||
main = "-jar"
|
||||
workingDir = rootDir
|
||||
args = listOfNotNull(
|
||||
configurations.getByName("apiGenerator").asPath,
|
||||
"generate-client-code",
|
||||
"--output-language",
|
||||
outputLanguage.argName,
|
||||
"--specification-path",
|
||||
pathToApi,
|
||||
"--kotlin-methods-generation-mode".takeIf { outputLanguage.methodOutputType != null },
|
||||
outputLanguage.methodOutputType?.argName,
|
||||
"--output-path",
|
||||
extension.outputDirPath,
|
||||
"--package-name",
|
||||
extension.outputPackageName,
|
||||
"--recreate_output_dirs",
|
||||
recreateOutputDir.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInputChanges(inputChanges: InputChanges) {
|
||||
val modifiedFiles = hashSetOf<File>()
|
||||
|
||||
inputChanges.getFileChanges(inputFiles)
|
||||
.filter { it.fileType == FileType.FILE }
|
||||
.forEach { change ->
|
||||
when (change.changeType) {
|
||||
ChangeType.REMOVED -> {
|
||||
outputFiles.find { it.nameWithoutExtension == change.file.nameWithoutExtension }?.delete()
|
||||
}
|
||||
|
||||
else -> modifiedFiles.add(change.file)
|
||||
}
|
||||
}
|
||||
|
||||
if (modifiedFiles.isEmpty()) return
|
||||
|
||||
handleModifiedFiles(modifiedFiles)
|
||||
}
|
||||
|
||||
private fun handleModifiedFiles(modifiedFiles: MutableCollection<File>) {
|
||||
val dependencyResolver = ApiModelsDependencyResolver(
|
||||
pathToApi = pathToApiScheme,
|
||||
fileParser = FileParser()
|
||||
)
|
||||
|
||||
modifiedFiles.addAll(
|
||||
dependencyResolver.getDependenciesFromFiles(modifiedFiles)
|
||||
)
|
||||
|
||||
val tempApiDirectory = tempApiDirDirectory.getOrElse("")
|
||||
|
||||
if (tempApiDirectory.isBlank()) {
|
||||
throw IllegalStateException("TempApiDirDirectory is blank or not specified.")
|
||||
}
|
||||
|
||||
copyAllFilesToTempDir(
|
||||
tempDirPath = tempApiDirectory,
|
||||
files = modifiedFiles
|
||||
)
|
||||
|
||||
generateApi(
|
||||
pathToApi = tempApiDirectory,
|
||||
outputLanguage = outputLanguage,
|
||||
recreateOutputDir = false
|
||||
)
|
||||
}
|
||||
|
||||
private fun copyAllFilesToTempDir(tempDirPath: String, files: Collection<File>) {
|
||||
val workQueue = getWorkerExecutor().noIsolation()
|
||||
|
||||
files.forEach { file ->
|
||||
val newFilePath = pathToApiScheme.toPath()
|
||||
.relativize(file.absolutePath.toPath()).toString()
|
||||
|
||||
val newFileFullPath = Path.of(tempDirPath, newFilePath).toString()
|
||||
|
||||
workQueue.submit(CopyApiModelsToTempDirTask::class.java) {
|
||||
getSourceFile().set(file)
|
||||
getTargetFilePath().set(newFileFullPath)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package apigen.depencency_resolver
|
||||
|
||||
import apigen.depencency_resolver.FileParser.Companion.NAME_JSON_KEY
|
||||
import groovy.json.JsonSlurper
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Класс для разрешения зависимостей АПИ моделей от других АПИ моделей.
|
||||
*
|
||||
* @param pathToApi путь к директории АПИ, где будут искаться зависимости
|
||||
* @param fileParser класс, ответственный за поиск типов в АПИ модельке.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class ApiModelsDependencyResolver(
|
||||
pathToApi: String,
|
||||
private val fileParser: FileParser
|
||||
) {
|
||||
|
||||
// Мапа для хранения и быстрого поиска типа модельки по файлам, т.к. не всегда имя файла совпадает с типом модельки.
|
||||
private val modelTypes: HashMap<String, File> = hashMapOf()
|
||||
private val handledFiles = hashSetOf<File>()
|
||||
|
||||
init {
|
||||
File(pathToApi)
|
||||
.walk()
|
||||
.filter { it.isFile }
|
||||
.forEach { file ->
|
||||
val json = JsonSlurper().parse(file) as? Map<String, Any?>
|
||||
val type = json?.getOrDefault(NAME_JSON_KEY, file.nameWithoutExtension).toString()
|
||||
|
||||
modelTypes[type] = file
|
||||
}
|
||||
}
|
||||
|
||||
fun getDependenciesFromFiles(files: Collection<File>): Set<File> =
|
||||
files.fold(setOf()) { dependencies, file ->
|
||||
dependencies + getFileDependencies(file)
|
||||
}
|
||||
|
||||
private fun getFileDependencies(file: File): Set<File> {
|
||||
if (handledFiles.contains(file)) {
|
||||
return emptySet()
|
||||
}
|
||||
|
||||
handledFiles.add(file)
|
||||
|
||||
val json = JsonSlurper().parse(file) as Map<String, Any?>
|
||||
val files = fileParser.getAllTypeNamesFromJson(json)
|
||||
.map { type ->
|
||||
modelTypes[type]
|
||||
?: throw IllegalArgumentException("Couldn't resolve $type in ${file.name}")
|
||||
}
|
||||
.toSet()
|
||||
|
||||
return files.fold(files) { dependencies, myFile ->
|
||||
dependencies + getFileDependencies(myFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package apigen.depencency_resolver
|
||||
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.workers.WorkAction
|
||||
import org.gradle.workers.WorkParameters
|
||||
import java.io.File
|
||||
|
||||
abstract class CopyApiModelsToTempDirTask: WorkAction<CopyApiModelsToTempDirTaskParams> {
|
||||
|
||||
override fun execute() {
|
||||
val sourceFile = parameters.getSourceFile().asFile.get()
|
||||
val target = File(parameters.getTargetFilePath().get())
|
||||
|
||||
sourceFile.copyTo(target = target)
|
||||
}
|
||||
}
|
||||
|
||||
interface CopyApiModelsToTempDirTaskParams: WorkParameters {
|
||||
fun getSourceFile(): RegularFileProperty
|
||||
fun getTargetFilePath(): Property<String>
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package apigen.depencency_resolver
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class FileParser {
|
||||
|
||||
companion object {
|
||||
const val NAME_JSON_KEY = "name"
|
||||
const val PARENT_JSON_KEY = "parent"
|
||||
const val TYPE_JSON_KEY = "type"
|
||||
}
|
||||
|
||||
private val defaultTypes = hashSetOf(
|
||||
"Bool",
|
||||
"String",
|
||||
"DateTime",
|
||||
"Long",
|
||||
"Int",
|
||||
"Double",
|
||||
"Decimal",
|
||||
"Date",
|
||||
"DateTimeTimestamp",
|
||||
"Color",
|
||||
"StringDecimal",
|
||||
"Url",
|
||||
"Map",
|
||||
"null" // Костыль, чтобы не писать проверки на null при получении типа из мапы
|
||||
)
|
||||
|
||||
fun getAllTypeNamesFromJson(model: Map<String, Any?>): Set<String> {
|
||||
val currentModelType = Type.getType(model[NAME_JSON_KEY].toString())
|
||||
|
||||
var generic = ""
|
||||
|
||||
if (currentModelType is TypeWithGeneric) {
|
||||
generic = currentModelType.getGenericTypeName()
|
||||
}
|
||||
|
||||
val parentRawTypeName = model[PARENT_JSON_KEY].toString()
|
||||
|
||||
return model.entries
|
||||
.fold(setOf(parentRawTypeName)) { foundRawTypeNames: Set<String>, jsonEntry: Map.Entry<String, Any?> ->
|
||||
foldDependencies(foundRawTypeNames, jsonEntry)
|
||||
}
|
||||
.minus(defaultTypes)
|
||||
.minus(generic)
|
||||
.fold(setOf()) { foundTypeNames, rawTypeName -> foundTypeNames + getTypeFromRawTypeNames(rawTypeName) }
|
||||
}
|
||||
|
||||
private fun foldDependencies(foundTypes: Set<String>, jsonEntry: Map.Entry<String, Any?>): Set<String> {
|
||||
val value = jsonEntry.value
|
||||
|
||||
val newTypes: Set<String> = when {
|
||||
(value as? Collection<Map<String, Any?>>) != null -> {
|
||||
getTypeFromCollection(value)
|
||||
}
|
||||
|
||||
(value as? Map<String, Any?>) != null && jsonEntry.key == TYPE_JSON_KEY -> {
|
||||
val typeName = value[NAME_JSON_KEY].toString()
|
||||
val parentTypeName = value[PARENT_JSON_KEY].toString()
|
||||
|
||||
setOf(typeName, parentTypeName)
|
||||
}
|
||||
|
||||
else -> setOf()
|
||||
}
|
||||
|
||||
return if (newTypes.isEmpty()) {
|
||||
foundTypes
|
||||
} else {
|
||||
foundTypes + newTypes
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTypeFromCollection(value: Collection<Map<String, Any?>>): Set<String> =
|
||||
value.fold(setOf()) { types: Set<String>, collectionEntry: Map<String, Any?> ->
|
||||
types + getAllTypeNamesFromJson(collectionEntry)
|
||||
}
|
||||
|
||||
private fun getTypeFromRawTypeNames(typeName: String): Set<String> {
|
||||
val type = Type.getType(typeName)
|
||||
val newTypeNames = mutableSetOf<String>()
|
||||
|
||||
when (type) {
|
||||
is SimpleType -> {
|
||||
newTypeNames.add(type.getMainTypeName())
|
||||
}
|
||||
|
||||
is ArrayType -> {
|
||||
newTypeNames.add(type.getMainTypeName())
|
||||
}
|
||||
|
||||
is MapType -> {
|
||||
newTypeNames.addAll(type.getSubTypeNames())
|
||||
}
|
||||
|
||||
is TypeWithGeneric -> {
|
||||
newTypeNames.add(type.getMainTypeName())
|
||||
}
|
||||
}
|
||||
|
||||
return newTypeNames
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package apigen.depencency_resolver
|
||||
|
||||
sealed class Type(protected val typeName: String) {
|
||||
|
||||
companion object {
|
||||
private val simpleTypeRegex = Regex("\\w*")
|
||||
private val mapRegex = Regex("Map<.*?,.*?>")
|
||||
private val arrayRegex = Regex(".*?\\[?]")
|
||||
private val typeWithGenericRegex = Regex(".*?<.*?>")
|
||||
|
||||
fun getType(typeName: String): Type = when {
|
||||
typeName.matches(mapRegex) -> MapType(typeName)
|
||||
typeName.matches(arrayRegex) -> ArrayType(typeName)
|
||||
typeName.matches(typeWithGenericRegex) -> TypeWithGeneric(typeName)
|
||||
typeName.matches(simpleTypeRegex) -> SimpleType(typeName)
|
||||
else -> throw IllegalArgumentException("Cannot define type for $typeName")
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getMainTypeName(): String
|
||||
}
|
||||
|
||||
// CardV2
|
||||
class SimpleType(typeName: String) : Type(typeName) {
|
||||
|
||||
override fun getMainTypeName(): String = typeName
|
||||
}
|
||||
|
||||
// String[]
|
||||
class ArrayType(typeName: String): Type(typeName) {
|
||||
|
||||
override fun getMainTypeName(): String = typeName.dropLast(2)
|
||||
}
|
||||
|
||||
// BaseResponse<T>
|
||||
class TypeWithGeneric(typeName: String): Type(typeName) {
|
||||
|
||||
override fun getMainTypeName(): String = typeName
|
||||
.substringBefore("<")
|
||||
|
||||
fun getGenericTypeName(): String = typeName
|
||||
.substringAfter("<")
|
||||
.dropLast(1)
|
||||
}
|
||||
|
||||
// Subscription<String, Int>
|
||||
class MapType(typeName: String): Type(typeName) {
|
||||
|
||||
override fun getMainTypeName(): String = typeName.substringBefore("<")
|
||||
|
||||
fun getSubTypeNames(): Set<String> = typeName
|
||||
.substringAfter("<")
|
||||
.dropLast(1)
|
||||
.split(",")
|
||||
.map { it.trim() }
|
||||
.toSet()
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
package static_analysis.linters
|
||||
|
||||
import com.android.build.gradle.AppExtension
|
||||
import com.android.build.gradle.AppPlugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.findByType
|
||||
import static_analysis.errors.AndroidLintError
|
||||
import static_analysis.errors.StaticAnalysisError
|
||||
import static_analysis.plugins.StaticAnalysisExtension
|
||||
|
|
@ -33,22 +31,10 @@ class AndroidLinter : Linter {
|
|||
.flatten()
|
||||
|
||||
override fun setupForProject(project: Project, extension: StaticAnalysisExtension) {
|
||||
project.beforeEvaluate {
|
||||
subprojects
|
||||
.mapNotNull { it.extensions.findByType<AppExtension>() }
|
||||
.first()
|
||||
.lintOptions.apply {
|
||||
isAbortOnError = false
|
||||
isCheckAllWarnings = true
|
||||
isWarningsAsErrors = false
|
||||
xmlReport = true
|
||||
htmlReport = false
|
||||
isCheckDependencies = true
|
||||
disable("MissingConstraints", "VectorRaster")
|
||||
xmlOutput = getLintReportFile()
|
||||
lintConfig = file("${extension.buildScriptDir}/static_analysis_configs/lint.xml")
|
||||
}
|
||||
}
|
||||
// Make sure to set lint options manually in modules gradle file
|
||||
// Otherwise you will get java.io.FileNotFoundException
|
||||
|
||||
// See issue: https://github.com/TouchInstinct/BuildScripts/issues/310
|
||||
}
|
||||
|
||||
override fun getTaskNames(project: Project, buildType: String?): List<String> {
|
||||
|
|
@ -62,11 +48,14 @@ class AndroidLinter : Linter {
|
|||
.mapNotNull { subproject: Project ->
|
||||
subproject
|
||||
.tasks
|
||||
.find { task -> task.name.contains(buildType, ignoreCase = true) && task.name.contains("lint") }
|
||||
?.path
|
||||
.filter { task ->
|
||||
task.name.equals("lint${buildType}", ignoreCase = true)
|
||||
|| task.name.equals("copy${buildType}AndroidLintReports", ignoreCase = true)
|
||||
}
|
||||
.map { it.path }
|
||||
}
|
||||
.flatten()
|
||||
}
|
||||
|
||||
private fun Project.getLintReportFile() = file("${rootProject.buildDir}/reports/lint-report.xml")
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,24 @@ package static_analysis.linters
|
|||
|
||||
import io.gitlab.arturbosch.detekt.Detekt
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.file.FileTree
|
||||
import static_analysis.errors.DetektError
|
||||
import static_analysis.errors.StaticAnalysisError
|
||||
import static_analysis.plugins.StaticAnalysisExtension
|
||||
import static_analysis.utils.getSources
|
||||
import static_analysis.utils.runCommand
|
||||
import static_analysis.utils.typedChildren
|
||||
import static_analysis.utils.xmlParser
|
||||
import java.io.File
|
||||
|
||||
class DetektLinter : Linter {
|
||||
|
||||
private companion object {
|
||||
const val TAG = "DetektLinter"
|
||||
const val ONLY_DIFFS_FLAG = "only-diffs"
|
||||
const val GET_GIT_DIFFS_COMMAND = "git diff --name-only --ignore-submodules"
|
||||
}
|
||||
|
||||
override val name: String = "Detekt"
|
||||
|
||||
override fun getErrors(project: Project): List<StaticAnalysisError> = xmlParser(project.getDetektReportFile())
|
||||
|
|
@ -50,11 +59,39 @@ class DetektLinter : Linter {
|
|||
}
|
||||
}
|
||||
|
||||
source = getSources(extension.excludes)
|
||||
val diffsBranch = properties[ONLY_DIFFS_FLAG] as? String
|
||||
source = getSources(extension.excludes, diffsBranch, project)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSources(excludes: String, diffsBranch: String?, project: Project): FileTree = when (diffsBranch) {
|
||||
null -> project.getSources(excludes)
|
||||
else -> getGitDiffFiles(excludes, diffsBranch, project)
|
||||
}
|
||||
|
||||
private fun getGitDiffFiles(excludes: String, diffsBranch: String, project: Project): FileTree {
|
||||
val getGitDiffsCommand = if (diffsBranch.isEmpty()) {
|
||||
GET_GIT_DIFFS_COMMAND
|
||||
} else {
|
||||
GET_GIT_DIFFS_COMMAND.plus(" --merge-base $diffsBranch")
|
||||
}
|
||||
|
||||
val gitDiffs = getGitDiffsCommand.runCommand()
|
||||
|
||||
if (gitDiffs.isNullOrEmpty()) {
|
||||
project.logger.error("$TAG: Diffs are empty or specified branch or commit does not exists")
|
||||
return project.files().asFileTree
|
||||
}
|
||||
|
||||
val diffFiles = gitDiffs.lines()
|
||||
.map { File(it) }
|
||||
.filter { (it.extension == "kt" || it.extension == "java") && !excludes.contains(it.path) }
|
||||
.toList()
|
||||
|
||||
return project.files(diffFiles).asFileTree
|
||||
}
|
||||
|
||||
override fun getTaskNames(project: Project, buildType: String?): List<String> = listOf(":detekt")
|
||||
|
||||
private fun Project.getDetektReportFile() = file("${rootProject.buildDir}/reports/detekt.xml")
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package static_analysis.plugins
|
|||
import com.android.build.gradle.AppExtension
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
import static_analysis.linters.AndroidLinter
|
||||
import static_analysis.linters.CpdLinter
|
||||
import static_analysis.linters.DetektLinter
|
||||
import static_analysis.linters.Linter
|
||||
|
|
@ -26,6 +27,23 @@ class StaticAnalysisAndroidPlugin : StaticAnalysisPlugin() {
|
|||
buildVariant = applicationVariants.first { it.name.contains("Debug") }.name
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Task to run detekt checks.
|
||||
*
|
||||
* @param -Ponly-diffs <branch or commit> if specified, only files modified
|
||||
* relative to this branch or commit will be checked. If specified without value
|
||||
* then current uncommited changes will be checked. If not specified all source files will be checked.
|
||||
* @see DetektLinter.getGitDiffFiles, 'git diff' for more info.
|
||||
* */
|
||||
project.tasks.register("detektAnalysis") {
|
||||
val detektLinter = linters.find { it is DetektLinter }
|
||||
?: throw IllegalStateException("DetektLinter not found")
|
||||
|
||||
setupStaticAnalysisTask(
|
||||
linters = listOf(detektLinter),
|
||||
buildVariant = applicationVariants.first { it.name.contains("Debug") }.name
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,8 +52,7 @@ class StaticAnalysisAndroidPlugin : StaticAnalysisPlugin() {
|
|||
override fun createLinters(): List<Linter> = listOf(
|
||||
DetektLinter(),
|
||||
CpdLinter(),
|
||||
//TODO temporary disable Android Linter to avoid FileNotFoundException when generating report
|
||||
//AndroidLinter()
|
||||
AndroidLinter()
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package static_analysis.utils
|
||||
|
||||
import apigen.ApiGeneratorExtension
|
||||
import apigen.ApiGeneratorPlugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.file.FileTree
|
||||
import java.io.File
|
||||
|
|
@ -25,3 +27,8 @@ fun Project.getSources(excludes: String): FileTree = files(
|
|||
.filter { it.exists() && it.isDirectory }
|
||||
.map { it.path }
|
||||
).asFileTree
|
||||
|
||||
fun Project.getApiGeneratorExtension(): ApiGeneratorExtension =
|
||||
extensions.getByName(ApiGeneratorPlugin.API_GENERATOR_EXT_NAME) as ApiGeneratorExtension
|
||||
|
||||
fun Project.getProjectName() = rootDir.absolutePath.substringAfterLast(File.separator)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package static_analysis.utils
|
||||
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
fun String.runCommand(
|
||||
directoryToExecute: File? = null,
|
||||
timeoutSec: Long = 30,
|
||||
): String? {
|
||||
return try {
|
||||
val parts = this.split("\\s".toRegex())
|
||||
val process = ProcessBuilder(*parts.toTypedArray())
|
||||
.directory(directoryToExecute)
|
||||
.redirectOutput(ProcessBuilder.Redirect.PIPE)
|
||||
.redirectError(ProcessBuilder.Redirect.PIPE)
|
||||
.start()
|
||||
|
||||
process.waitFor(timeoutSec, TimeUnit.SECONDS)
|
||||
process.inputStream.bufferedReader().readText()
|
||||
} catch(e: IOException) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun String.toPath(): Path = Path.of(this)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
android {
|
||||
lint {
|
||||
abortOnError false
|
||||
checkAllWarnings true
|
||||
warningsAsErrors false
|
||||
checkDependencies true
|
||||
htmlReport false
|
||||
textReport false
|
||||
xmlReport true
|
||||
xmlOutput file("${rootProject.buildDir}/reports/lint-report.xml")
|
||||
lintConfig file("${rootProject.ext["buildScriptsDir"]}/static_analysis_configs/lint.xml")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,15 @@
|
|||
import groovy.json.JsonSlurper
|
||||
import groovy.xml.MarkupBuilder
|
||||
|
||||
task stringGenerator {
|
||||
generate(android.languageMap, project)
|
||||
println("Strings generated!")
|
||||
tasks.register('stringGenerator') {
|
||||
def sources = android.languageMap
|
||||
|
||||
outputs.files(sources)
|
||||
|
||||
doLast {
|
||||
generate(sources, project)
|
||||
println("Strings generated!")
|
||||
}
|
||||
}
|
||||
|
||||
private def generate(Map<String, String> sources, Project project) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@
|
|||
<!--All activities should have locked orientation-->
|
||||
<issue id="LockedOrientationActivity" severity="ignore" />
|
||||
|
||||
<!-- TODO: Update Timber version. See this issue: https://github.com/JakeWharton/timber/issues/408 -->
|
||||
<issue id="WrongTimberUsageDetector" severity="ignore" />
|
||||
|
||||
<issue id="AllowAllHostnameVerifier" severity="error" />
|
||||
<issue id="InvalidUsesTagAttribute" severity="error" />
|
||||
<issue id="MissingIntentFilterForMediaSearch" severity="error" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue