diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a0882ca
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+# User-specific stuff:
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/dictionaries
+
+# Sensitive or high-churn files:
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+
+# Gradle:
+.idea/**/gradle.xml
+.idea/**/libraries
+
+## File-based project format:
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+out/
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..9fa41d4
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..7d511af
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StAnalysisKotlin.iml b/StAnalysisKotlin.iml
new file mode 100644
index 0000000..e025b20
--- /dev/null
+++ b/StAnalysisKotlin.iml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StAnalysisKotlin.jar b/StAnalysisKotlin.jar
new file mode 100644
index 0000000..e3120a4
Binary files /dev/null and b/StAnalysisKotlin.jar differ
diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml
new file mode 100644
index 0000000..fb80bfd
--- /dev/null
+++ b/resources/META-INF/plugin.xml
@@ -0,0 +1,46 @@
+
+ ru.touchin.staticanalysis
+ Static analysis runner
+ 2.0
+ Touch Instinct
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/resources/icons/logo.png b/resources/icons/logo.png
new file mode 100644
index 0000000..215e26d
Binary files /dev/null and b/resources/icons/logo.png differ
diff --git a/resources/icons/logo@2x.png b/resources/icons/logo@2x.png
new file mode 100644
index 0000000..0ea1cc8
Binary files /dev/null and b/resources/icons/logo@2x.png differ
diff --git a/src/icons/PluginIcons.java b/src/icons/PluginIcons.java
new file mode 100644
index 0000000..9a15f2f
--- /dev/null
+++ b/src/icons/PluginIcons.java
@@ -0,0 +1,11 @@
+package icons;
+
+import com.intellij.openapi.util.IconLoader;
+
+import javax.swing.*;
+
+public interface PluginIcons {
+
+ Icon PLUGIN_ICON = IconLoader.getIcon("/icons/logo.png");
+
+}
diff --git a/src/ru/touchin/staticanalysis/actions/RunStaticAnalysis.kt b/src/ru/touchin/staticanalysis/actions/RunStaticAnalysis.kt
new file mode 100644
index 0000000..919713a
--- /dev/null
+++ b/src/ru/touchin/staticanalysis/actions/RunStaticAnalysis.kt
@@ -0,0 +1,22 @@
+package ru.touchin.staticanalysis.actions
+
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.actionSystem.PlatformDataKeys
+import ru.touchin.staticanalysis.tasks.AnalysisTask
+
+class RunStaticAnalysis : AnAction("Touchin static analysis") {
+
+ private lateinit var analysisTask: AnalysisTask
+
+ override fun actionPerformed(actionEvent: AnActionEvent) {
+ val project = actionEvent.getData(PlatformDataKeys.PROJECT)!!
+ analysisTask = AnalysisTask(project)
+ analysisTask.queue()
+ }
+
+ override fun update(actionEvent: AnActionEvent) {
+ actionEvent.presentation.isEnabled = !::analysisTask.isInitialized || !analysisTask.isRunning
+ }
+
+}
\ No newline at end of file
diff --git a/src/ru/touchin/staticanalysis/notifications/ConsoleService.kt b/src/ru/touchin/staticanalysis/notifications/ConsoleService.kt
new file mode 100644
index 0000000..e9b07b8
--- /dev/null
+++ b/src/ru/touchin/staticanalysis/notifications/ConsoleService.kt
@@ -0,0 +1,11 @@
+package ru.touchin.staticanalysis.notifications
+
+import com.intellij.execution.filters.TextConsoleBuilderFactory
+import com.intellij.execution.ui.ConsoleView
+import com.intellij.openapi.project.Project
+
+class ConsoleService(project: Project) {
+
+ val consoleView: ConsoleView = TextConsoleBuilderFactory.getInstance().createBuilder(project).console
+
+}
\ No newline at end of file
diff --git a/src/ru/touchin/staticanalysis/notifications/LogToolWindowFactory.kt b/src/ru/touchin/staticanalysis/notifications/LogToolWindowFactory.kt
new file mode 100644
index 0000000..1889add
--- /dev/null
+++ b/src/ru/touchin/staticanalysis/notifications/LogToolWindowFactory.kt
@@ -0,0 +1,16 @@
+package ru.touchin.staticanalysis.notifications
+
+import com.intellij.openapi.components.ServiceManager
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.wm.ToolWindow
+import com.intellij.openapi.wm.ToolWindowFactory
+
+class LogToolWindowFactory : ToolWindowFactory {
+
+ override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
+ val consoleView = ServiceManager.getService(project, ConsoleService::class.java).consoleView
+ val content = toolWindow.contentManager.factory.createContent(consoleView.component, "", true)
+ toolWindow.contentManager.addContent(content)
+ }
+
+}
\ No newline at end of file
diff --git a/src/ru/touchin/staticanalysis/notifications/PluginNotificationManager.kt b/src/ru/touchin/staticanalysis/notifications/PluginNotificationManager.kt
new file mode 100644
index 0000000..c5757de
--- /dev/null
+++ b/src/ru/touchin/staticanalysis/notifications/PluginNotificationManager.kt
@@ -0,0 +1,37 @@
+package ru.touchin.staticanalysis.notifications
+
+import com.intellij.notification.*
+import com.intellij.openapi.project.Project
+import icons.PluginIcons
+
+class PluginNotificationManager(private val project: Project) {
+
+ fun showErrorNotification(message: String) {
+ hideOldNotifications()
+ NotificationGroup(NOTIFICATION_TITLE, NotificationDisplayType.STICKY_BALLOON, true)
+ .createNotification(NOTIFICATION_TITLE, message, NotificationType.ERROR, null)
+ .notify(project)
+ }
+
+ fun showInfoNotification(message: String) {
+ hideOldNotifications()
+ NotificationGroup(NOTIFICATION_TITLE, NotificationDisplayType.STICKY_BALLOON, true, null, PluginIcons.PLUGIN_ICON)
+ Notification(NOTIFICATION_TITLE, PluginIcons.PLUGIN_ICON, NOTIFICATION_TITLE, null, message, NotificationType.INFORMATION, null)
+ .notify(project)
+ }
+
+ private fun hideOldNotifications() {
+ val logModel = EventLog.getLogModel(project)
+ for (notification in logModel.notifications) {
+ if (notification.groupId == NOTIFICATION_TITLE) {
+ logModel.removeNotification(notification)
+ notification.expire()
+ }
+ }
+ }
+
+ companion object {
+ private const val NOTIFICATION_TITLE = "Static Analysis"
+ }
+
+}
\ No newline at end of file
diff --git a/src/ru/touchin/staticanalysis/tasks/AnalysisTask.kt b/src/ru/touchin/staticanalysis/tasks/AnalysisTask.kt
new file mode 100644
index 0000000..bb04d51
--- /dev/null
+++ b/src/ru/touchin/staticanalysis/tasks/AnalysisTask.kt
@@ -0,0 +1,116 @@
+package ru.touchin.staticanalysis.tasks
+
+import com.intellij.execution.ui.ConsoleViewContentType
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.components.ServiceManager
+import com.intellij.openapi.progress.ProcessCanceledException
+import com.intellij.openapi.progress.ProgressIndicator
+import com.intellij.openapi.progress.Task.Backgroundable
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.wm.IdeFrame
+import com.intellij.openapi.wm.ToolWindowManager
+import com.intellij.openapi.wm.WindowManager
+import com.intellij.ui.AppIcon
+import ru.touchin.staticanalysis.notifications.ConsoleService
+import ru.touchin.staticanalysis.notifications.PluginNotificationManager
+import java.io.BufferedReader
+import java.io.File
+import java.io.InputStreamReader
+import java.util.regex.Pattern
+
+
+class AnalysisTask(project: Project) : Backgroundable(project, "StaticAnalysis", true) {
+
+ var isRunning: Boolean = false
+ private val notificationsManager = PluginNotificationManager(project)
+
+ private val gradlewCommand = if (System.getProperty("os.name").startsWith("Windows"))
+ listOf("cmd", "/c", "gradlew.bat", "staticAnalys")
+ else
+ listOf("./gradlew", "staticAnalys")
+
+ private val gradlewProcess: Process by lazy {
+ ProcessBuilder(gradlewCommand)
+ .directory(File(project.basePath))
+ .redirectErrorStream(true)
+ .start()
+ }
+
+ override fun run(progressIndicator: ProgressIndicator) {
+ isRunning = true
+ progressIndicator.isIndeterminate = true
+ try {
+ runAnalysis(progressIndicator)
+ } catch (canceledException: ProcessCanceledException) {
+ progressIndicator.cancel()
+ } catch (exception: Exception) {
+ notificationsManager.showErrorNotification("Exception: " + exception.message)
+ }
+ }
+
+ override fun onCancel() {
+ gradlewProcess.destroy()
+ }
+
+ override fun onFinished() {
+ isRunning = false
+ }
+
+ @Throws(Exception::class)
+ private fun runAnalysis(progressIndicator: ProgressIndicator) {
+ val analysisOutput: String = getAnalysisOutput(progressIndicator)
+ if (!analysisOutput.startsWith("Error") && !analysisOutput.startsWith("FAILURE")) {
+ if (Pattern.compile("Overall: PASSED").matcher(analysisOutput).find()) {
+ notificationsManager.showInfoNotification("Overall: PASSED!")
+ requestIdeFocus()
+ } else if (!Pattern.compile("Overall: FAILED").matcher(analysisOutput).find()) {
+ notificationsManager.showErrorNotification("Can't detect analysis result. Try to run it manually.")
+ } else {
+ val errorsCountPattern = Pattern.compile("Overall: FAILED \\((.+)\\)")
+ val errorsCountMatcher = errorsCountPattern.matcher(analysisOutput)
+ if (errorsCountMatcher.find()) {
+ notificationsManager.showErrorNotification(String.format("Analysis failed: %s", errorsCountMatcher.group(1)))
+ ApplicationManager.getApplication().invokeLater {
+ ToolWindowManager.getInstance(project).getToolWindow("Static Analysis Log").show(null)
+ }
+ } else {
+ notificationsManager.showErrorNotification("Can't detect analysis result. Try to run it manually.")
+ }
+ }
+ } else {
+ notificationsManager.showErrorNotification(analysisOutput)
+ }
+ }
+
+ @Throws(Exception::class)
+ private fun getAnalysisOutput(progressIndicator: ProgressIndicator): String {
+ val bufferedReader = BufferedReader(InputStreamReader(gradlewProcess.inputStream))
+ val analysisOutputBuilder = StringBuilder()
+
+ val consoleView = ServiceManager.getService(project, ConsoleService::class.java).consoleView
+ var outputLine: String? = bufferedReader.readLine()
+ while (outputLine != null) {
+
+ consoleView.print(outputLine + '\n', ConsoleViewContentType.NORMAL_OUTPUT)
+ progressIndicator.text2 = outputLine
+ progressIndicator.checkCanceled()
+ analysisOutputBuilder.append(outputLine)
+ analysisOutputBuilder.append('\n')
+ outputLine = bufferedReader.readLine()
+ }
+
+ return analysisOutputBuilder.toString()
+ }
+
+ private fun requestIdeFocus() {
+ ApplicationManager.getApplication().invokeLater {
+ val frame = WindowManager.getInstance().getFrame(project)
+ if (frame is IdeFrame) {
+ AppIcon.getInstance().requestFocus(frame)
+ AppIcon.getInstance().requestAttention(project, true)
+ AppIcon.getInstance().setOkBadge(project, true)
+ }
+ }
+ }
+
+}
\ No newline at end of file