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