diff --git a/plugins/jetbrains_plugin/.run/Run IDE with Plugin.run.xml b/plugins/jetbrains_plugin/.run/Run IDE with Plugin.run.xml new file mode 100644 index 000000000..7747a2940 --- /dev/null +++ b/plugins/jetbrains_plugin/.run/Run IDE with Plugin.run.xml @@ -0,0 +1,24 @@ + + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/plugins/jetbrains_plugin/README.md b/plugins/jetbrains_plugin/README.md new file mode 100644 index 000000000..827332434 --- /dev/null +++ b/plugins/jetbrains_plugin/README.md @@ -0,0 +1,72 @@ +# IDEA插件简介 +**idea插件是一个可以帮助你快速在idea内部启动代码分析并展示分析结果的插件, 你只需要配置一些分析必要的参数, 就可以在本地进行代码分析, 并且可以在idea中以树形结构和标记代码行这些比较直观的方式来查看错误** +# 使用教程 +## 前置步骤 +**1、安装python3.7, 部署TCA开源版, 在开源版中创建团队和分析方案模板** + +**2、在TCA源码的 CodeAnalysis 目录下执行`bash ./scripts/base/install_bin.sh`命令** + +**3、配置客户端 config.ini 文件, 打开TCA源码的 `CodeAnalysis/client/config.ini` 文件, 将 config.ini 文件里的``替换成部署的TCA平台的IP (可包含端口号)** +## 安装插件 +**在插件源码的TCA目录下执行`gradle build`命令进行构建, 构建成功后会生成zip文件`TCA/build/distributions/TCA-1.0.zip`, 在idea中的插件安装页面点击设置图标, 选择从磁盘安装插件, 选择生成的TCA-1.0.zip文件并打开就能安装, 安装后选择重启idea插件就会生效** + +## 配置参数 +**python3路径: python3的安装路径, 安装python后执行`which python3`可以查看python3的安装路径.注意python3的版本要求是3.7** + +**TCA源码安装路径: 拉取到本地的CodeAnalysis项目的绝对路径 (例如:/data/CodeAnalysis/)** + +**个人Token: 在部署的TCA服务的【个人中心】->【个人令牌】中获取** + +**分析方案模板ID: TCA服务里创建的分析方案模板的ID, 在分析方案模板的“基础配置”中获取** + +**团队唯一标识: 在TCA服务中团队的团队概览页中获取** +## 启动分析 +**idea插件有多种方式启动分析** + +**1、分析指定文件: 在打开的文件页右键或在idea左侧的项目结构树中对指定文件右键, 在右键弹出的窗口中会有一个`当前路径开启TCA代码分析`选项, 点击按钮就可以分析指定文件** + +**2、分析指定目录: 在idea左侧的项目结构树里对指定目录右键, 弹出的窗口中也会有一个`当前路径开启TCA代码分析`选项, 点击就可以分析指定目录下的所有文件** + +**3、重新运行上一次目录或文件分析: 安装插件并重启后在idea底部会增加一个窗口, 窗口中的绿色启动按钮的功能就是重新运行上一次目录或文件分析, 点击后会重新执行上一次右键启动的分析** + +**4、工程分析: 在窗口中有一个蓝色启动按钮, 点击后即可对整个项目的文件进行分析** + +## 查看分析日志和结果 +**成功安装idea插件并重启后, 在idea底部会增加一个窗口, 窗口中有两个标签页, 分别是分析日志和分析结果, 启动分析后分析日志页会不停的打印分析日志, 可以根据分析日志查看分析进度和查看分析是否出现异常.** + +**分析结果页在分析结束后会以树形结构的方式展示错误信息, 树形结构的根节点是根据用户选择的节点设置的, 在树中会显示目录、文件的问题总数, 以及每个文件的错误列表, 双击错误列表中的具体错误信息可以跳转到出现该错误的代码行, 此外在代码行的头部也会展示对应的错误信息.** + +**需要注意的是, 分析日志是追加的形式不断插入到分析日志窗口里的, 而分析结果每一次更新都会覆盖掉前一次的分析结果** + +# 源码理解 +**本部分主要介绍如何理解此插件的源码** + +**idea插件可以启动代码分析的基础是`python3 codepuppy.py quickinit -t TOKEN --scheme-template-id SCHEME_TEMPLATE_ID --org-sid ORG_SID`和`python3 codepuppy.py quickscan -t TOKEN --scheme-template-id SCHEME_TEMPLATE_ID --org-sid ORG_SID -s SOURCE_DIR --file FILE`命令, 这两个命令的功能是初始化工具和执行扫描.** +**idea插件启动分析本质上就是用代码执行这两个命令, 所以idea插件能够正常运行的前提是这两个命令能够正常运行. 代码分析命令执行完后会在client目录下生成分析结果文件`tca_quick_scan_report.json`, idea插件会获取这个文件里的数据, 并进行一系列的处理, 然后展示到idea界面** + +## 插件源码结构 +### 前置概念 +**在idea插件中使用到了idea的一些插件开发概念, 包括服务、窗口和动作.** + +**服务: 用于实现应用程序级或项目级功能的一种机制, 可以理解为由交由idea维护的一种应用级或项目级的单例功能. 在TCA插件中我使用应用级服务维护了设置参数信息, 持久化了参数数据, 也让用户的数据可以在idea的所有项目中通用** + +**窗口: 就是展示给用户的窗口. 在idea插件中使用UI组件实例的方式创建窗口, 举例: 创建窗口的步骤一般为创建一个页面类的实例, 创建一些UI组件的实例比如文本框、按钮, 然后把这些组件实例添加到页面实例里, 返回这个页面实例给idea, idea就会把页面渲染出来. 需要注意的是idea有一些布局管理器, 比如`BorderLayout`类, 他会把页面分成五个区域, 添加组件实例时可以指定添加到对应区域** + +**动作: 具体功能的实现, 动作在idea中一般以按钮的方式展示, 此插件中启动分析的按钮就是一个动作, 可以在配置文件中设置动作按钮的展示位置** + +### 代码结构 +**插件源码中比较重要的文件是TCA目录下的build.gradle.kts文件和src目录下的所有文件, build.gradle.kts文件主要定义了插件的依赖、构建任务以及其他构建配置, 比如当想让插件使用一些尚未引入的依赖时就可以在这个文件里配置** + +**src目录下的文件定义了插件的功能和逻辑, src目录下resource目录中主要包含了用到的图标文件和插件配置文件`plugin.xml`, 配置文件中配置了插件的一些战术信息, 此外配置文件还管理了插件服务、窗口和动作的注册** + +**src目录下的java目录则时插件核心功能的实现, java目录下有五个目录, 说一下每个目录包含的功能** + +**Action目录: 所有的动作在这里实现包括ClearLogs(清除日志)、MarkCodeLine(标记错误代码行)、MenuSelect(右键代码分析动作), RestartLastAnalysis(重新运行上一次目录或文件分析), StartDirectoryAnalysis(进行目录分析), StartFileAnalysis(进行文件分析), StartProjectAnalysis(进行工程分析)** + +**Data目录: 主要用来定义存储数据的类, ProjectErrorData(从分析结果文件里获取数据的类)、ErrorData(存储结果树叶子节点的类)、FileType(存储结果树非叶子节点的类)、MarkCodeMessage(存储标记错误代码数据的类)** + +**Listener目录: 监听器类, 这里主要是监听文件的打开操作, 打开文件后在代码行里标记错误信息** + +**Setting目录: 用于实现插件的设置功能, AppSettings(实现设置功能的类, 作为应用级服务在配置文件中注册、主要用来持久化存储设置参数)、AppSettingsComponent(编写设置页面的类)、AppSettingsConfigurable(实现设置功能, 包括更新设置和检测参数是否更改等功能, 同样在配置文件中注册)** + +**Window目录: 编写插件窗口, MenuFactory(窗口的工厂类, 在配置文件中注册, 工厂类会获取要展示的页面的实例)、TreeNodeType(用来修改结果树展示样式的类)** \ No newline at end of file diff --git a/plugins/jetbrains_plugin/build.gradle.kts b/plugins/jetbrains_plugin/build.gradle.kts new file mode 100644 index 000000000..7820f4f01 --- /dev/null +++ b/plugins/jetbrains_plugin/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + id("java") + id("org.jetbrains.kotlin.jvm") version "1.9.24" + id("org.jetbrains.intellij") version "1.17.3" +} + +group = "org.example" +version = "1.0" + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.fasterxml.jackson.core:jackson-databind:2.13.5") + implementation("com.fasterxml.jackson.core:jackson-core:2.13.5") + implementation("com.fasterxml.jackson.core:jackson-annotations:2.13.5") +} + +// Configure Gradle IntelliJ Plugin +// Read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html +intellij { + version.set("2024.1.4") + type.set("IC") // Target IDE Platform + + plugins.set(listOf(/* Plugin Dependencies */)) +} + +tasks { + // Set the JVM compatibility versions + withType { + sourceCompatibility = "17" + targetCompatibility = "17" + } + withType { + kotlinOptions.jvmTarget = "17" + } + + patchPluginXml { + sinceBuild.set("232") + } + + signPlugin { + certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) + privateKey.set(System.getenv("PRIVATE_KEY")) + password.set(System.getenv("PRIVATE_KEY_PASSWORD")) + } + + publishPlugin { + token.set(System.getenv("PUBLISH_TOKEN")) + } +} diff --git a/plugins/jetbrains_plugin/gradle.properties b/plugins/jetbrains_plugin/gradle.properties new file mode 100644 index 000000000..24630b3f8 --- /dev/null +++ b/plugins/jetbrains_plugin/gradle.properties @@ -0,0 +1,6 @@ +# Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib +kotlin.stdlib.default.dependency=false +# Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html +org.gradle.configuration-cache=true +# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html +org.gradle.caching=true diff --git a/plugins/jetbrains_plugin/gradle/wrapper/gradle-wrapper.jar b/plugins/jetbrains_plugin/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..249e5832f Binary files /dev/null and b/plugins/jetbrains_plugin/gradle/wrapper/gradle-wrapper.jar differ diff --git a/plugins/jetbrains_plugin/gradle/wrapper/gradle-wrapper.properties b/plugins/jetbrains_plugin/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..48c0a02ca --- /dev/null +++ b/plugins/jetbrains_plugin/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/plugins/jetbrains_plugin/gradlew b/plugins/jetbrains_plugin/gradlew new file mode 100644 index 000000000..1b6c78733 --- /dev/null +++ b/plugins/jetbrains_plugin/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/plugins/jetbrains_plugin/gradlew.bat b/plugins/jetbrains_plugin/gradlew.bat new file mode 100644 index 000000000..ac1b06f93 --- /dev/null +++ b/plugins/jetbrains_plugin/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/plugins/jetbrains_plugin/settings.gradle.kts b/plugins/jetbrains_plugin/settings.gradle.kts new file mode 100644 index 000000000..281b4caf9 --- /dev/null +++ b/plugins/jetbrains_plugin/settings.gradle.kts @@ -0,0 +1,8 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "jetbrains_plugin" \ No newline at end of file diff --git a/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/ClearLogs.java b/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/ClearLogs.java new file mode 100644 index 000000000..36c28edb0 --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/ClearLogs.java @@ -0,0 +1,22 @@ +package com.TCA.Action; + +import com.TCA.Window.ProjectAnalysisLogsWindow; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.util.IconLoader; +import org.jetbrains.annotations.NotNull; + +//继承AnAction类, 为动作类 +public class ClearLogs extends AnAction { + + public ClearLogs() { + // 设置动作的名称和图标,在工具栏或菜单中显示 + super("清除日志", "", IconLoader.getIcon("/icons/clearLogs.svg", ClearLogs.class)); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + //清除日志功能 + ProjectAnalysisLogsWindow.clearLogs(e.getProject().getBasePath()); + } +} diff --git a/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/MarkCodeLine.java b/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/MarkCodeLine.java new file mode 100644 index 000000000..e8e70dbe4 --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/MarkCodeLine.java @@ -0,0 +1,155 @@ +package com.TCA.Action; + +import com.TCA.Data.ErrorData; +import com.TCA.Data.MarkCodeMessage; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.TextEditor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.IconLoader; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.JBColor; + +import javax.swing.*; +import java.io.File; +import java.util.*; + +//此类的主要作用是在代码行中标记错误代码 +public class MarkCodeLine { + + //用来存储标记错误信息的map, 存储了所有文件、所有代码行的错误信息, 参数分别代表 文件路径、行数、错误信息, 注意结构为map嵌套map + private static final Map> markMessageMap = new HashMap<>(); + //给错误级别设置权制, 便于比较 + public static final Map cmp = new HashMap<>(); + static { + cmp.put("fatal", 4); + cmp.put("error", 3); + cmp.put("warning", 2); + cmp.put("info", 1); + } + + //往markMessageMap中添加数据, markMessageMap是本类中定义的map + public static void addFileMarkMessage(ErrorData errorData) { + if(!markMessageMap.containsKey(errorData.filePath)) { + //如果当前文件没有存储错误信息的Map实例, 就创建一个添加到markMessageMap中 + List list = new ArrayList<>(); + list.add(errorData.severity + ": " + errorData.msg); + + Map markMessage = new HashMap<>(); + + markMessage.put(errorData.line, new MarkCodeMessage(errorData.filePath, errorData.line, errorData.severity, list)); + markMessageMap.put(errorData.filePath, markMessage); + } else { + //获取当前文件的Map实例 + Map markMessage = markMessageMap.get(errorData.filePath); + if (!markMessage.containsKey(errorData.line)) { + //如果Map实例里没有对应代码行的MarkCodeMessage实例, 就创建一个并放到Map实例里 + List list = new ArrayList<>(); + list.add(errorData.severity + ": " + errorData.msg); + markMessage.put(errorData.line, new MarkCodeMessage(errorData.filePath, errorData.line, errorData.severity, list)); + } else { + //如果对应代码行已经有MarkCodeMessage实例存储错误信息就更新MarkCodeMessage实例中的错误信息 + MarkCodeMessage markCodeMessage = markMessage.get(errorData.line); + markCodeMessage.errorMessage.add(errorData.severity + ": " + errorData.msg); + if (cmp.get(errorData.severity) > cmp.get(markCodeMessage.severity)) + markCodeMessage.severity = errorData.severity; + } + } + } + + + //标记某个文件里出现错误的代码行 + public static void showMark(String filePath, Project project) { + //获取对应文件的编辑器 + FileEditorManager fileEditorManager = FileEditorManager.getInstance(project); + VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByPath(filePath.replace(File.separatorChar, '/')); + FileEditor[] fileEditors = fileEditorManager.getEditors(virtualFile); + if(fileEditors.length == 0) + return; + Editor editor = null; + for (FileEditor fileEditor : fileEditors) { + if (fileEditor instanceof TextEditor) { + editor = ((TextEditor) fileEditor).getEditor(); + } + } + if(editor == null) + return; + //获取对应文件的错误信息map + Map markMessage = markMessageMap.get(filePath); + if(markMessage == null) + return; + //遍历错误信息map, 对每个出现错误的代码行分别处理 + for (Map.Entry entry : markMessage.entrySet()) { + action(entry.getValue(), editor); + } + markMessageMap.remove(filePath); + } + + //标记某一行代码行 + private static void action(MarkCodeMessage markCodeMessage, Editor editor) { + + MarkupModel markupModel = editor.getMarkupModel(); + //移除之前此代码行里的标记 + markupModel.removeAllHighlighters(); + + //设置标记样式 + TextAttributes attributes = new TextAttributes(); + if(cmp.get(markCodeMessage.severity) > 2) + attributes.setEffectColor(JBColor.RED); + else + attributes.setEffectColor(JBColor.yellow); + attributes.setEffectType(EffectType.LINE_UNDERSCORE); + + int startOffset = editor.getDocument().getLineStartOffset(markCodeMessage.codeLine - 1); + int endOffset = editor.getDocument().getLineEndOffset(markCodeMessage.codeLine - 1); + + RangeHighlighter highlighter = markupModel.addRangeHighlighter( + startOffset, endOffset, 0, attributes, HighlighterTargetArea.EXACT_RANGE); + + //设置标记代码行的图标 + highlighter.setGutterIconRenderer(new GutterIconRenderer() { + //获取图标 + @Override + public Icon getIcon() { + if(cmp.get(markCodeMessage.severity) > 2) + return IconLoader.getIcon("/icons/error.svg", StartFileAnalysis.class);// 使用错误图标 + else + return IconLoader.getIcon("/icons/warning.svg", StartFileAnalysis.class); + } + + //设置鼠标悬停在图标上时展示的文本 + @Override + public String getTooltipText() { + String message = null; + for(String msg: markCodeMessage.errorMessage) + if (message == null) + message = msg; + else + message = message + "
" + msg + "
"; + return message; // 提供错误信息作为工具提示文本 + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + return true; + } + + @Override + public int hashCode() { + return Objects.hash(getClass()); + } + + //定义点击图标要执行的操作 + @Override + public AnAction getClickAction() { + return null; + } + }); + } +} \ No newline at end of file diff --git a/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/MenuSelect.java b/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/MenuSelect.java new file mode 100644 index 000000000..8ad9682da --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/MenuSelect.java @@ -0,0 +1,87 @@ +package com.TCA.Action; + +import com.TCA.Setting.AppSettings; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.Presentation; +import com.intellij.openapi.util.IconLoader; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.Objects; + +//此类是动作类, 用于实现右键启动分析的功能, 已在配置文件中注册 +public class MenuSelect extends AnAction { + + //用于更新按钮的可用状态 + private static Boolean isEnable = true; + + //构造函数, 设置按钮的图标和文本 + public MenuSelect() { + // 设置动作的名称和图标,在工具栏或菜单中显示 + super("当前路径开启TCA代码分析", "", IconLoader.getIcon("/icons/run.svg", MenuSelect.class)); + } + + //更新按钮状态, 设置按钮是否可用 + @Override + public void update(@NotNull AnActionEvent event) { + Presentation presentation = event.getPresentation(); + presentation.setEnabled(isEnable); + } + + //设置按钮不可用 + private void unUseButton(AnActionEvent event) { + isEnable = false; + update(event); + Presentation presentation = event.getPresentation(); + presentation.setIcon(IconLoader.getIcon("/icons/canNotRun.svg", StartFileAnalysis.class)); + } + + //设置按钮可用 + private void useButton(AnActionEvent event) { + isEnable = true; + update(event); + Presentation presentation = event.getPresentation(); + presentation.setIcon(IconLoader.getIcon("/icons/run.svg", StartFileAnalysis.class)); + } + + //此动作类的入口, 点击按钮会先执行此函数 + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + //设置按钮不可用 + unUseButton(event); + //获取参数设置服务的实例 + AppSettings.State state = Objects.requireNonNull(AppSettings.getInstance().getState()); + + // 获取文件或目录的路径 + VirtualFile virtualFile = event.getData(CommonDataKeys.VIRTUAL_FILE); + String filePath; + if (virtualFile != null) { + filePath = virtualFile.getPath(); + } else { + filePath = event.getProject().getBasePath(); + } + if(filePath == null) { + System.out.println(filePath); + return; + } + //存储路径到设置服务, 为重新运行上一次右键分析做准备 + state.sourceDir = filePath; + File file = new File(filePath); + + //判断是在文件上右键还是在目录上右键 + if (file.isDirectory()) { + StartDirectoryAnalysis startDirectoryAnalysis = new StartDirectoryAnalysis(); + startDirectoryAnalysis.actionPerformed(event); + + } else { + StartFileAnalysis startFileAnalysis = new StartFileAnalysis(); + startFileAnalysis.actionPerformed(event); + } + + //设置按钮可用 + useButton(event); + } +} diff --git a/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/RestartLastAnalysis.java b/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/RestartLastAnalysis.java new file mode 100644 index 000000000..ec46791f6 --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/RestartLastAnalysis.java @@ -0,0 +1,65 @@ +package com.TCA.Action; + +import com.TCA.Setting.AppSettings; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.Presentation; +import com.intellij.openapi.util.IconLoader; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.Objects; + +//此类是动作类, 用于实现重新运行上一次右键分析的功能 +public class RestartLastAnalysis extends AnAction { + private static Boolean isEnable = true; + + public RestartLastAnalysis() { + // 设置动作的名称和图标,在工具栏或菜单中显示 + super("重新运行上一次目录或文件分析", "", IconLoader.getIcon("/icons/run.svg", MenuSelect.class)); + } + + //更新按钮状态 + @Override + public void update(@NotNull AnActionEvent event) { + Presentation presentation = event.getPresentation(); + presentation.setEnabled(isEnable); + } + + //设置按钮不可用 + private void unUseButton(AnActionEvent event) { + isEnable = false; + update(event); + Presentation presentation = event.getPresentation(); + presentation.setIcon(IconLoader.getIcon("/icons/canNotRun.svg", StartFileAnalysis.class)); + } + + //设置按钮可用 + private void useButton(AnActionEvent event) { + isEnable = true; + update(event); + Presentation presentation = event.getPresentation(); + presentation.setIcon(IconLoader.getIcon("/icons/run.svg", StartFileAnalysis.class)); + } + + //此动作类的入口, 点击按钮执行此函数 + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + //设置按钮不可用 + unUseButton(event); + //获取参数设置服务的实例 + AppSettings.State state = Objects.requireNonNull(AppSettings.getInstance().getState()); + File file = new File(state.sourceDir); + //判断file是目录还是文件, 执行对应的分析方法 + if (file.isDirectory()) { + StartDirectoryAnalysis startDirectoryAnalysis = new StartDirectoryAnalysis(); + startDirectoryAnalysis.actionPerformed(event); + + } else { + StartFileAnalysis startFileAnalysis = new StartFileAnalysis(); + startFileAnalysis.actionPerformed(event); + } + //设置按钮可用 + useButton(event); + } +} diff --git a/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/StartDirectoryAnalysis.java b/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/StartDirectoryAnalysis.java new file mode 100644 index 000000000..9aa40c027 --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/StartDirectoryAnalysis.java @@ -0,0 +1,263 @@ +package com.TCA.Action; + +import com.TCA.Data.ErrorData; +import com.TCA.Data.FileType; +import com.TCA.Data.ProjectErrorData; +import com.TCA.Setting.AppSettings; +import com.TCA.Window.ProjectAnalysisDataWindow; +import com.TCA.Window.ProjectAnalysisLogsWindow; +import com.TCA.Window.TreeNodeType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.editor.CaretModel; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.LogicalPosition; +import com.intellij.openapi.editor.ScrollType; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.treeStructure.Tree; +import org.jetbrains.annotations.NotNull; + +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreePath; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +//此类是普通类, 用于被动作类调用, 实现了目录分析的功能 +public class StartDirectoryAnalysis { + + //DFS建立结果树 + void addFileNode(DefaultMutableTreeNode root, File file, Map> map, String bashPath, FileType fileType, AnActionEvent event) { + if(file.isDirectory()) { + //如果当前文件是目录 + for (File f: file.listFiles()) { + //遍历目录下所有文件 + DefaultMutableTreeNode node; + FileType childFileType; + //根据f是否是目录创建不同的FileType实例, 以便后面能够区分树上节点是否是目录 + if (f.isDirectory()) { + childFileType = new FileType(f.getName(), true); + node = new DefaultMutableTreeNode(childFileType); + } + else { + childFileType = new FileType(f.getName(), false); + node = new DefaultMutableTreeNode(childFileType); + } + root.add(node); + addFileNode(node, f, map, bashPath, childFileType, event); + //把孩子节点的错误数量加到父亲节点上 + fileType.fatal = fileType.fatal + childFileType.fatal; + fileType.error = fileType.error + childFileType.error; + fileType.warning = fileType.warning + childFileType.warning; + fileType.info = fileType.info + childFileType.info; + fileType.sum = fileType.sum + childFileType.sum; + } + } else { + //如果当前文件不是目录 + if(file.getPath().length() >= bashPath.length()) { + //获取当前文件错误数据 + List list = map.get(file.getPath().substring(bashPath.length())); + if(list == null) + return; + for (ErrorData errorData: list) { + //遍历文件的所有错误数据 + errorData.filePath = file.getPath(); + //把错误数据添加到存储标记代码行数据的map中 + MarkCodeLine.addFileMarkMessage(errorData); + DefaultMutableTreeNode node = new DefaultMutableTreeNode(errorData); + root.add(node); + //根据错误种类修改文件的错误数 + fileType.countIssue(errorData.severity); + } + //把该文件的错误数据从map中移除 + map.remove(file.getPath().substring(bashPath.length())); + //标记该文件中出现错误的代码行 + MarkCodeLine.showMark(file.getPath(), event.getProject()); + } + } + } + + //创建结果树 + private void createDataTree(AnActionEvent event, String dataPath, String basePath) { + ObjectMapper objectMapper = new ObjectMapper(); + File file = new File(basePath); + + ProjectErrorData projectErrorData; + try { + //从分析结果文件里面读取数据, 根据数据生成ProjectErrorData实例 + projectErrorData = objectMapper.readValue(new File(dataPath), ProjectErrorData.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + + //获取问题详情列表 + Map> map = projectErrorData.issue_detail; + + //创建结果树的根节点 + FileType rootFileType = new FileType(file.getName(), true); + FileType otherFileType = new FileType("OtherError", true); + //创建结果树的根节点, DefaultMutableTreeNode是树的节点, FileType是树节点的数据 + DefaultMutableTreeNode root = new DefaultMutableTreeNode(rootFileType); + DefaultMutableTreeNode other = new DefaultMutableTreeNode(otherFileType); + //执行DFS建树, 统计目录和文件错误数量, 标记错误代码行 + addFileNode(root, file, map, basePath, rootFileType, event); + + //建完树之后剩下的错误数据不属于单个文件, 而是属于整个项目, 把他们添加到OtherError节点下 + for(Map.Entry> entry: map.entrySet()) { + for (ErrorData errorData: entry.getValue()) { + //把不属于单个文件的错误行数设为-1 + errorData.line = -1; + otherFileType.countIssue(errorData.severity); + DefaultMutableTreeNode node = new DefaultMutableTreeNode(errorData); + other.add(node); + } + } + root.add(other); + //调用函数删除结果树中没有出现错误的目录或文件 + TreeNodeType.deleteBlankNode(root); + //使用DefaultMutableTreeNode类创建Tree实例 + Tree tree = new Tree(root); + //配置双击跳转功能 + tree.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { // 检测双击事件 + // 获取双击位置的树路径 + TreePath path = tree.getPathForLocation(e.getX(), e.getY()); + if (path != null) { + // 获取双击的节点 + DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); + ErrorData errorData = (ErrorData) node.getUserObject(); + //等于-1代表错误不属于某个文件 + if(errorData.line == -1) + return; + jumpToLine(errorData, event, errorData.filePath); + } + } + } + }); + //设置结果树的展示样式 + tree.setCellRenderer(new TreeNodeType()); + //更新结果窗口中的结果树 + ProjectAnalysisDataWindow.setDataTree(tree, event.getProject().getBasePath()); + } + + private void jumpToLine(ErrorData errorData, AnActionEvent event, String filePath) { + //跳转到出现错误的行 + //获取要跳转的文件的editor + Project project = event.getProject(); + FileEditorManager fileEditorManager = FileEditorManager.getInstance(project); + VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByPath(filePath.replace(File.separatorChar, '/')); + Editor editor = fileEditorManager.openTextEditor(new OpenFileDescriptor(project, virtualFile), true); + + //使用editor设置光标的位置 + if (editor != null) { + CaretModel caretModel = editor.getCaretModel(); + LogicalPosition pos = new LogicalPosition(errorData.line - 1, errorData.column - 1); + caretModel.moveToLogicalPosition(pos); + editor.getScrollingModel().scrollToCaret(ScrollType.CENTER); + } + } + + public void actionPerformed(@NotNull AnActionEvent event) { + //获取执行此函数的project环境和参数设置服务的实例 + Project project = event.getProject(); + AppSettings.State state = Objects.requireNonNull(AppSettings.getInstance().getState()); + + //脚本的工作目录 + File workDir = new File(state.codeAnalysisClientPath); + //初始化工具命令 + String initCommand = ""; + initCommand = initCommand + state.ePythonPath; + initCommand = initCommand + " codepuppy.py"; + initCommand = initCommand + " quickinit"; + initCommand = initCommand + " -t " + state.token; + initCommand = initCommand + " --scheme-template-id " + state.schemeTemplateId; + initCommand = initCommand + " --org-sid " + state.orgSid; + + //快速分析命令 + String runCommand = ""; + runCommand = runCommand + state.ePythonPath; + runCommand = runCommand + " codepuppy.py"; + runCommand = runCommand + " quickscan"; + runCommand = runCommand + " -t " + state.token; + runCommand = runCommand + " --scheme-template-id " + state.schemeTemplateId; + runCommand = runCommand + " --org-sid " + state.orgSid; + runCommand = runCommand + " -s " + state.sourceDir; + + String finalRunCommand = runCommand; + String finalInitCommand = initCommand; + //创建线程执行命令 + new Task.Backgroundable(project, "执行目录下全文件分析", false) { + @Override + public void run(@NotNull ProgressIndicator indicator) { + //使用ProcessBuilder运行脚本 + ProcessBuilder processBuilder = new ProcessBuilder(); + //设置要执行的脚本 + String OS = System.getProperty("os.name").toLowerCase(); + //获取操作系统环境变量并变为小些 + if(OS.contains("win")) + processBuilder.command("cmd", "/c", finalInitCommand + "&&" + finalRunCommand); + else + processBuilder.command("bash", "-c", finalInitCommand + "&&" + finalRunCommand); + //设置工作目录 + processBuilder.directory(workDir); + //更新ProcessBuilder新建进程的环境变量 + Map environment = processBuilder.environment(); + environment.put("PATH", state.pythonPath + ":" + environment.get("PATH")); + + Process process; + try { + //执行线程 + process = processBuilder.start(); + //创建两个新的线程读取线程的输出 + new Thread(() -> { + try { + String line; + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + while ((line = reader.readLine()) != null) { + //把输出打印到分析日志标签页 + ProjectAnalysisLogsWindow.addText(line, project.getBasePath()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }).start(); + + new Thread(() ->{ + try { + String line; + BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); + while ((line = errorReader.readLine()) != null) { + //把输出打印到分析日志标签页 + ProjectAnalysisLogsWindow.addText(line, project.getBasePath()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }).start(); + //等待process线程执行结束 + process.waitFor(); + //创建结果树 + createDataTree(event, state.codeAnalysisClientPath + "/tca_quick_scan_report.json", state.sourceDir + "/"); + } catch (Exception e) { + //打印错误信息到日志窗口 + ProjectAnalysisLogsWindow.addText(e.toString(), project.getBasePath()); + throw new RuntimeException(e); + } + } + }.queue(); + } +} diff --git a/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/StartFileAnalysis.java b/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/StartFileAnalysis.java new file mode 100644 index 000000000..95a1a17ba --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/StartFileAnalysis.java @@ -0,0 +1,192 @@ +package com.TCA.Action; + +import com.TCA.Data.ErrorData; +import com.TCA.Data.FileType; +import com.TCA.Data.ProjectErrorData; +import com.TCA.Setting.AppSettings; +import com.TCA.Window.ProjectAnalysisDataWindow; +import com.TCA.Window.ProjectAnalysisLogsWindow; +import com.TCA.Window.TreeNodeType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.editor.CaretModel; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.LogicalPosition; +import com.intellij.openapi.editor.ScrollType; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.treeStructure.Tree; +import org.jetbrains.annotations.NotNull; + +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreePath; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +//此类是普通类, 用于被动作类调用, 实现了文件分析的功能 +//具体实现与StartDirectoryAnalysis类似, 可参考StartDirectoryAnalysis中的解释 +public class StartFileAnalysis { + + private void createDataTree(AnActionEvent event, File file, String dataPath) { + ObjectMapper objectMapper = new ObjectMapper(); + String fileName = file.getName(); + + FileType rootFile = new FileType(fileName, true); + DefaultMutableTreeNode root = new DefaultMutableTreeNode(rootFile); + ProjectErrorData projectErrorData; + try { + //从分析结果文件里面读取数据, 根据数据生成ProjectErrorData实例 + projectErrorData = objectMapper.readValue(new File(dataPath), ProjectErrorData.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + DefaultMutableTreeNode node; + //文件分析中结果树的层数有限, 最多是三层, 不需要使用DFS构建 + //遍历分析出的所有错误数据 + for(Map.Entry> entry: projectErrorData.issue_detail.entrySet()) { + for (ErrorData errorData : entry.getValue()) { + if(entry.getKey().equals(fileName)) { + + errorData.filePath = file.getPath(); + rootFile.countIssue(errorData.severity); + + node = new DefaultMutableTreeNode(errorData); + root.add(node); + MarkCodeLine.addFileMarkMessage(errorData); + } + } + MarkCodeLine.showMark(file.getPath(), event.getProject()); + } + TreeNodeType.deleteBlankNode(root); + Tree tree = new Tree(root); + tree.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { // 检测双击事件 + // 获取双击位置的树路径 + TreePath path = tree.getPathForLocation(e.getX(), e.getY()); + if (path != null) { + // 获取双击的节点node + DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); + ErrorData errorData = (ErrorData) node.getUserObject(); + if(errorData.line == -1) + return; + jumpToLine(errorData, event, file.getPath()); + } + } + } + }); + + tree.setCellRenderer(new TreeNodeType()); + + ProjectAnalysisDataWindow.setDataTree(tree, event.getProject().getBasePath()); + } + + private void jumpToLine(ErrorData errorData, AnActionEvent event, String filePath) { + Project project = event.getProject(); + FileEditorManager fileEditorManager = FileEditorManager.getInstance(project); + VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByPath(filePath.replace(File.separatorChar, '/')); + Editor editor = fileEditorManager.openTextEditor(new OpenFileDescriptor(project, virtualFile), true); + + if (editor != null) { + CaretModel caretModel = editor.getCaretModel(); + LogicalPosition pos = new LogicalPosition(errorData.line - 1, errorData.column - 1); + caretModel.moveToLogicalPosition(pos); + editor.getScrollingModel().scrollToCaret(ScrollType.CENTER); + } + } + +// @Override + public void actionPerformed(AnActionEvent event) { + + AppSettings.State state = Objects.requireNonNull(AppSettings.getInstance().getState()); + Project project = event.getProject(); + + File file = new File(state.sourceDir); + + File workDir = new File(state.codeAnalysisClientPath); + String initCommand = ""; + initCommand = initCommand + state.ePythonPath; + initCommand = initCommand + " codepuppy.py"; + initCommand = initCommand + " quickinit"; + initCommand = initCommand + " -t " + state.token; + initCommand = initCommand + " --scheme-template-id " + state.schemeTemplateId; + initCommand = initCommand + " --org-sid " + state.orgSid; + + String runCommand = ""; + runCommand = runCommand + state.ePythonPath; + runCommand = runCommand + " codepuppy.py"; + runCommand = runCommand + " quickscan"; + runCommand = runCommand + " -t " + state.token; + runCommand = runCommand + " --scheme-template-id " + state.schemeTemplateId; + runCommand = runCommand + " --org-sid " + state.orgSid; + runCommand = runCommand + " -s " + file.getPath().substring(0, file.getPath().length() - file.getName().length()); + runCommand = runCommand + " --file " + file.getName(); + + String finalRunCommand = runCommand; + String finalInitCommand = initCommand; + new Task.Backgroundable(project, "执行代码分析", false) { + @Override + public void run(@NotNull ProgressIndicator indicator) { + + System.out.println(finalInitCommand); + System.out.println(finalRunCommand); + ProcessBuilder processBuilder = new ProcessBuilder(); + //获取操作系统环境变量并变为小些 + String OS = System.getProperty("os.name").toLowerCase(); + if(OS.contains("win")) + processBuilder.command("cmd", "/c", finalInitCommand + "&&" + finalRunCommand); + else + processBuilder.command("bash", "-c", finalInitCommand + "&&" + finalRunCommand); + processBuilder.directory(workDir); + Map environment = processBuilder.environment(); + environment.put("PATH", state.pythonPath + ":" + environment.get("PATH")); + + Process process; + try { + process = processBuilder.start(); + new Thread(() -> { + try { + String line; + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + while ((line = reader.readLine()) != null) { + ProjectAnalysisLogsWindow.addText(line, project.getBasePath()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }).start(); + + new Thread(() ->{ + try { + String line; + BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); + while ((line = errorReader.readLine()) != null) { + ProjectAnalysisLogsWindow.addText(line, project.getBasePath()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }).start(); + process.waitFor(); + createDataTree(event, file, state.codeAnalysisClientPath + "/tca_quick_scan_report.json"); + } catch (Exception e) { + ProjectAnalysisLogsWindow.addText(e.toString(), project.getBasePath()); + throw new RuntimeException(e); + } + } + }.queue(); + } +} \ No newline at end of file diff --git a/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/StartProjectAnalysis.java b/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/StartProjectAnalysis.java new file mode 100644 index 000000000..71876d56c --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/java/com/TCA/Action/StartProjectAnalysis.java @@ -0,0 +1,262 @@ +package com.TCA.Action; + +import com.TCA.Data.ErrorData; +import com.TCA.Data.FileType; +import com.TCA.Data.ProjectErrorData; +import com.TCA.Setting.AppSettings; +import com.TCA.Window.ProjectAnalysisDataWindow; +import com.TCA.Window.ProjectAnalysisLogsWindow; +import com.TCA.Window.TreeNodeType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.Presentation; +import com.intellij.openapi.editor.CaretModel; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.LogicalPosition; +import com.intellij.openapi.editor.ScrollType; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.IconLoader; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.treeStructure.Tree; +import org.jetbrains.annotations.NotNull; + +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreePath; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +//此类是动作类, 实现了工程分析的功能 +//具体实现与StartDirectoryAnalysis类似, 可参考StartDirectoryAnalysis中的解释, 不同的是此类在配置文件中进行了注册, 用户可以直接使用此类, 而不是通过动作类调用此类 +public class StartProjectAnalysis extends AnAction{ + private static Boolean isEnable = true; + + public StartProjectAnalysis() { + // 设置动作的名称和图标,在工具栏或菜单中显示 + super("开启工程分析", "", IconLoader.getIcon("/icons/runProject.svg", MenuSelect.class)); + } + + @Override + public void update(@NotNull AnActionEvent event) { + Presentation presentation = event.getPresentation(); + presentation.setEnabled(isEnable); + } + + private void unUseButton(AnActionEvent event) { + isEnable = false; + update(event); + Presentation presentation = event.getPresentation(); + presentation.setIcon(IconLoader.getIcon("/icons/canNotRun.svg", StartFileAnalysis.class)); + } + + private void useButton(AnActionEvent event) { + isEnable = true; + update(event); + Presentation presentation = event.getPresentation(); + presentation.setIcon(IconLoader.getIcon("/icons/runProject.svg", StartFileAnalysis.class)); + } + + void addFileNode(DefaultMutableTreeNode root, File file, Map> map, String bashPath, FileType fileType, AnActionEvent event) { + if(file.isDirectory()) { + for (File f: file.listFiles()) { + DefaultMutableTreeNode node; + FileType childFileType; + if (f.isDirectory()) { + childFileType = new FileType(f.getName(), true); + node = new DefaultMutableTreeNode(childFileType); + } + else { + childFileType = new FileType(f.getName(), false); + node = new DefaultMutableTreeNode(childFileType); + } + root.add(node); + addFileNode(node, f, map, bashPath, childFileType, event); + fileType.fatal = fileType.fatal + childFileType.fatal; + fileType.error = fileType.error + childFileType.error; + fileType.warning = fileType.warning + childFileType.warning; + fileType.info = fileType.info + childFileType.info; + fileType.sum = fileType.sum + childFileType.sum; + } + } else { + if(file.getPath().length() >= bashPath.length()) { + List list = map.get(file.getPath().substring(bashPath.length())); + if(list == null) + return; + for (ErrorData errorData: list) { + + errorData.filePath = file.getPath(); + MarkCodeLine.addFileMarkMessage(errorData); + DefaultMutableTreeNode node = new DefaultMutableTreeNode(errorData); + root.add(node); + fileType.countIssue(errorData.severity); + } + map.remove(file.getPath().substring(bashPath.length())); + MarkCodeLine.showMark(file.getPath(), event.getProject()); + } + } + } + + private void createDataTree(AnActionEvent event, String dataPath) { + ObjectMapper objectMapper = new ObjectMapper(); + Project project = event.getProject(); + + ProjectErrorData projectErrorData; + try { + projectErrorData = objectMapper.readValue(new File(dataPath), ProjectErrorData.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + + Map> map = projectErrorData.issue_detail; + + FileType rootFileType = new FileType(project.getName(), true); + FileType otherFileType = new FileType("OtherError", true); + File file = new File(project.getBasePath()); + + DefaultMutableTreeNode root = new DefaultMutableTreeNode(rootFileType); + DefaultMutableTreeNode other = new DefaultMutableTreeNode(otherFileType); + addFileNode(root, file, map, project.getBasePath() + "/", rootFileType, event); + + for(Map.Entry> entry: map.entrySet()) { + for (ErrorData errorData: entry.getValue()) { + errorData.line = -1; + otherFileType.countIssue(errorData.severity); + DefaultMutableTreeNode node = new DefaultMutableTreeNode(errorData); + other.add(node); + } + } + root.add(other); + TreeNodeType.deleteBlankNode(root); + Tree tree = new Tree(root); + tree.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { // 检测双击事件 + // 获取双击位置的树路径 + TreePath path = tree.getPathForLocation(e.getX(), e.getY()); + + if (path != null) { + // 获取双击的节点 + DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); + if(node.getUserObject() instanceof ErrorData errorData) { + if(errorData.line == -1) + return; + jumpToLine(errorData, event, errorData.filePath); + } + } + } + } + }); + tree.setCellRenderer(new TreeNodeType()); + + ProjectAnalysisDataWindow.setDataTree(tree, event.getProject().getBasePath()); + } + + private void jumpToLine(ErrorData errorData, AnActionEvent event, String filePath) { + Project project = event.getProject(); + FileEditorManager fileEditorManager = FileEditorManager.getInstance(project); + VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByPath(filePath.replace(File.separatorChar, '/')); + Editor editor = fileEditorManager.openTextEditor(new OpenFileDescriptor(project, virtualFile), true); + + if (editor != null) { + CaretModel caretModel = editor.getCaretModel(); + LogicalPosition pos = new LogicalPosition(errorData.line - 1, errorData.column - 1); + caretModel.moveToLogicalPosition(pos); + editor.getScrollingModel().scrollToCaret(ScrollType.CENTER); + } + } + + public void actionPerformed(@NotNull AnActionEvent event) { + unUseButton(event); + + Project project = event.getProject(); + + AppSettings.State state = Objects.requireNonNull(AppSettings.getInstance().getState()); + + File workDir = new File(state.codeAnalysisClientPath); + String initCommand = ""; + initCommand = initCommand + state.ePythonPath; + initCommand = initCommand + " codepuppy.py"; + initCommand = initCommand + " quickinit"; + initCommand = initCommand + " -t " + state.token; + initCommand = initCommand + " --scheme-template-id " + state.schemeTemplateId; + initCommand = initCommand + " --org-sid " + state.orgSid; + + String runCommand = ""; + runCommand = runCommand + state.ePythonPath; + runCommand = runCommand + " codepuppy.py"; + runCommand = runCommand + " quickscan"; + runCommand = runCommand + " -t " + state.token; + runCommand = runCommand + " --scheme-template-id " + state.schemeTemplateId; + runCommand = runCommand + " --org-sid " + state.orgSid; + runCommand = runCommand + " -s " + project.getBasePath(); + + String finalRunCommand = runCommand; + String finalInitCommand = initCommand; + new Task.Backgroundable(project, "执行工程分析", false) { + @Override + public void run(@NotNull ProgressIndicator indicator) { + System.out.println(finalInitCommand); + System.out.println(finalRunCommand); + ProcessBuilder processBuilder = new ProcessBuilder(); + String OS = System.getProperty("os.name").toLowerCase(); + //获取操作系统环境变量并变为小些 + if(OS.contains("win")) + processBuilder.command("cmd", "/c", finalInitCommand + "&&" + finalRunCommand); + else + processBuilder.command("bash", "-c", finalInitCommand + "&&" + finalRunCommand); + processBuilder.directory(workDir); + Map environment = processBuilder.environment(); + environment.put("PATH", state.pythonPath + ":" + environment.get("PATH")); + + Process process; + try { + process = processBuilder.start(); + new Thread(() -> { + try { + String line; + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + while ((line = reader.readLine()) != null) { + ProjectAnalysisLogsWindow.addText(line, project.getBasePath()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }).start(); + + new Thread(() ->{ + try { + String line; + BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); + while ((line = errorReader.readLine()) != null) { + ProjectAnalysisLogsWindow.addText(line, project.getBasePath()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }).start(); + process.waitFor(); + createDataTree(event, state.codeAnalysisClientPath + "/tca_quick_scan_report.json"); + useButton(event); + } catch (Exception e) { + ProjectAnalysisLogsWindow.addText(e.toString(), project.getBasePath()); + throw new RuntimeException(e); + } finally { + useButton(event); + } + } + }.queue(); + } +} diff --git a/plugins/jetbrains_plugin/src/main/java/com/TCA/Listener/FileListener.java b/plugins/jetbrains_plugin/src/main/java/com/TCA/Listener/FileListener.java new file mode 100644 index 000000000..85ddc3640 --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/java/com/TCA/Listener/FileListener.java @@ -0,0 +1,18 @@ +package com.TCA.Listener; + +import com.TCA.Action.MarkCodeLine; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.FileEditorManagerListener; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; + +//用于监听文件打开操作的类 +public class FileListener implements FileEditorManagerListener { + + @Override + public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile file) { + //当文件打开时执行此函数, 标记打开文件里出现错误的代码行 + MarkCodeLine.showMark(file.getPath(), source.getProject()); + } +} diff --git a/plugins/jetbrains_plugin/src/main/java/com/TCA/Setting/AppSettings.java b/plugins/jetbrains_plugin/src/main/java/com/TCA/Setting/AppSettings.java new file mode 100644 index 000000000..71ad6c3ba --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/java/com/TCA/Setting/AppSettings.java @@ -0,0 +1,52 @@ +package com.TCA.Setting; + +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import com.intellij.openapi.application.ApplicationManager; +import org.jetbrains.annotations.NotNull; + +@State( + name = "org.intellij.sdk.settings.AppSettings", + storages = @Storage("SdkSettingsPlugin.xml") +) +//此类是用于持久化存储用户输入的设置参数的服务类, 已在配置文件中注册 +public final class AppSettings implements PersistentStateComponent { + + public static class State { + //去除python3的python安装路径 + public String pythonPath = ""; + //完整的python3安装路径 + public String ePythonPath; + //TCA源码路径 + public String codeAnalysisPath = ""; + //TCA源码下client目录的绝对路径 + public String codeAnalysisClientPath; + //个人token + public String token = ""; + //分析方案模板ID + public String schemeTemplateId = ""; + //团队唯一标识 + public String orgSid = ""; + //要分析的项目路径, 右键时会更新此参数, 在执行 重新运行上一次目录或文件分析 时会用到此参数 + public String sourceDir = ""; + //指定要分析的文件 + public String file = ""; + } + + private State myState = new State(); + + public static AppSettings getInstance() { + return ApplicationManager.getApplication().getService(AppSettings.class); + } + + @Override + public State getState() { + return myState; + } + + @Override + public void loadState(@NotNull State state) { + myState = state; + } +} diff --git a/plugins/jetbrains_plugin/src/main/java/com/TCA/Setting/AppSettingsComponent.java b/plugins/jetbrains_plugin/src/main/java/com/TCA/Setting/AppSettingsComponent.java new file mode 100644 index 000000000..a8f05f7a5 --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/java/com/TCA/Setting/AppSettingsComponent.java @@ -0,0 +1,88 @@ +package com.TCA.Setting; + +import com.intellij.ui.components.JBLabel; +import com.intellij.ui.components.JBTextField; +import com.intellij.util.ui.FormBuilder; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +//创建设置页面的类, 被AppSettingsConfigurable调用 +public class AppSettingsComponent { + //定义输入框 + private final JPanel myMainPanel; + private final JBTextField ePythonPath = new JBTextField(); + private final JBTextField codeAnalysisPath = new JBTextField(); + private final JBTextField token = new JBTextField(); + private final JBTextField schemeTemplateId = new JBTextField(); + private final JBTextField orgSid = new JBTextField(); + + //创建页面实例 + public AppSettingsComponent() { + myMainPanel = FormBuilder.createFormBuilder() + .addLabeledComponent(new JBLabel("Python3安装路径(python3版本应为3.7):"), ePythonPath, false) + .addComponent(new JBLabel()) + .addLabeledComponent(new JBLabel("TCA源码安装路径(以CodeAnalysis目录结束):"), codeAnalysisPath, false) + .addComponent(new JBLabel()) + .addLabeledComponent(new JBLabel("个人Token(从TCA的个人中心获取):"), token, false) + .addComponent(new JBLabel()) + .addLabeledComponent(new JBLabel("分析方案模版ID(TCA中创建方案并获取):"), schemeTemplateId, false) + .addComponent(new JBLabel()) + .addLabeledComponent(new JBLabel("团队唯一标识(TCA中创建团队并获取):"), orgSid, false) + .addComponent(new JBLabel()) + .addComponentFillVertically(new JPanel(), 1) + .getPanel(); + } + + //获取设置界面 + public JPanel getPanel() { + return myMainPanel; + } + + //get方法获取用户输入,set方法更新设置页显示的数据 + @NotNull + public String getePythonPath() { + return ePythonPath.getText(); + } + + public void setePythonPath(@NotNull String pythonPath) { + this.ePythonPath.setText(pythonPath); + } + + @NotNull + public String getCodeAnalysisPath() { + return codeAnalysisPath.getText(); + } + + public void setCodeAnalysisPath(@NotNull String codeAnalysisPath) { + this.codeAnalysisPath.setText(codeAnalysisPath); + } + + @NotNull + public String getToken() { + return token.getText(); + } + + public void setToken(@NotNull String token) { + this.token.setText(token); + } + + @NotNull + public String getSchemeTemplateId() { + return schemeTemplateId.getText(); + } + + public void setSchemeTemplateId(@NotNull String schemeTemplateId) { + this.schemeTemplateId.setText(schemeTemplateId); + } + + @NotNull + public String getOrgSid() { + return orgSid.getText(); + } + + public void setOrgSid(@NotNull String orgSid) { + this.orgSid.setText(orgSid); + } + +} \ No newline at end of file diff --git a/plugins/jetbrains_plugin/src/main/java/com/TCA/Setting/AppSettingsConfigurable.java b/plugins/jetbrains_plugin/src/main/java/com/TCA/Setting/AppSettingsConfigurable.java new file mode 100644 index 000000000..857a423fb --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/java/com/TCA/Setting/AppSettingsConfigurable.java @@ -0,0 +1,86 @@ +package com.TCA.Setting; + +import com.intellij.openapi.options.Configurable; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.Objects; + +final class AppSettingsConfigurable implements Configurable { + + private AppSettingsComponent mySettingsComponent; + + @Nls(capitalization = Nls.Capitalization.Title) + @Override + public String getDisplayName() { + return "TCA-分析参数配置"; + } + + @Override + public JComponent getPreferredFocusedComponent() { + //第一次打开设置页面时选中的组件 + return mySettingsComponent.getPanel(); + } + + @Nullable + @Override + public JComponent createComponent() { + //获取设置页的实例 + mySettingsComponent = new AppSettingsComponent(); + return mySettingsComponent.getPanel(); + } + + @Override + public boolean isModified() { + //检查设置是否被修改,修改后用户才能选择更新设置 + AppSettings.State state = Objects.requireNonNull(AppSettings.getInstance().getState()); + return !mySettingsComponent.getePythonPath().equals(state.ePythonPath) || + !mySettingsComponent.getCodeAnalysisPath().equals(state.codeAnalysisPath) || + !mySettingsComponent.getToken().equals(state.token) || + !mySettingsComponent.getSchemeTemplateId().equals(state.schemeTemplateId) || + !mySettingsComponent.getOrgSid().equals(state.orgSid); + } + + @Override + public void apply() { + //保存设置 + AppSettings.State state = Objects.requireNonNull(AppSettings.getInstance().getState()); + state.ePythonPath = mySettingsComponent.getePythonPath(); + //减去路径中的python3 + state.pythonPath = state.ePythonPath; + state.pythonPath = state.pythonPath.substring(0, state.pythonPath.length() - 7); + + state.codeAnalysisPath = mySettingsComponent.getCodeAnalysisPath(); + //拼接client源码路径 + state.codeAnalysisClientPath = state.codeAnalysisPath; + if(!state.codeAnalysisClientPath.startsWith("/")) + state.codeAnalysisClientPath = "/" + state.codeAnalysisClientPath; + if(!state.codeAnalysisClientPath.endsWith("/")) + state.codeAnalysisClientPath = state.codeAnalysisClientPath + "/"; + state.codeAnalysisClientPath = state.codeAnalysisClientPath + "client"; + + state.token = mySettingsComponent.getToken(); + state.schemeTemplateId = mySettingsComponent.getSchemeTemplateId(); + state.orgSid = mySettingsComponent.getOrgSid(); + reset(); + } + + @Override + public void reset() { + //重置设置到上次保存的状态 + AppSettings.State state = Objects.requireNonNull(AppSettings.getInstance().getState()); + mySettingsComponent.setePythonPath(state.ePythonPath); + mySettingsComponent.setCodeAnalysisPath(state.codeAnalysisPath); + mySettingsComponent.setToken(state.token); + mySettingsComponent.setSchemeTemplateId(state.schemeTemplateId); + mySettingsComponent.setOrgSid(state.orgSid); + } + + @Override + public void disposeUIResources() { + //释放UI资源 + mySettingsComponent = null; + } + +} \ No newline at end of file diff --git a/plugins/jetbrains_plugin/src/main/java/com/TCA/Window/MenuFactory.java b/plugins/jetbrains_plugin/src/main/java/com/TCA/Window/MenuFactory.java new file mode 100644 index 000000000..c6358b8ff --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/java/com/TCA/Window/MenuFactory.java @@ -0,0 +1,24 @@ +package com.TCA.Window; + +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowFactory; +import com.intellij.ui.content.Content; +import com.intellij.ui.content.ContentFactory; +import org.jetbrains.annotations.NotNull; + +//窗口工厂类, 此类在配置文件中注册, 用于实现在idea底部添加窗口 +public class MenuFactory implements ToolWindowFactory, DumbAware { + + //实现接口方法来设置页面样式 + @Override + public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) { + //创建ProjectMenu实例 + ProjectMenu projectMenu = new ProjectMenu(); + ContentFactory contentFactory = ContentFactory.getInstance(); + //使用ProjectMenu实例作为一个标签页 + Content proContent = contentFactory.createContent(projectMenu.createMenu(project), "本地代码分析", false); + toolWindow.getContentManager().addContent(proContent); + } +} \ No newline at end of file diff --git a/plugins/jetbrains_plugin/src/main/java/com/TCA/Window/ProjectAnalysisDataWindow.java b/plugins/jetbrains_plugin/src/main/java/com/TCA/Window/ProjectAnalysisDataWindow.java new file mode 100644 index 000000000..f041fafca --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/java/com/TCA/Window/ProjectAnalysisDataWindow.java @@ -0,0 +1,62 @@ +package com.TCA.Window; + +import com.TCA.Action.RestartLastAnalysis; +import com.TCA.Action.StartProjectAnalysis; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.project.Project; +import com.intellij.ui.components.JBScrollPane; +import com.intellij.ui.treeStructure.Tree; +import kotlinx.html.S; + +import javax.swing.*; +import java.awt.*; +import java.util.HashMap; +import java.util.Map; + +//此类用于创建分析结果标签页 +public class ProjectAnalysisDataWindow { + + //定义分析结果标签页map, 为每个打开的项目分配一个分析结果标签页 + private static final Map windows = new HashMap<>(); + //定义分析结果标签页的滚动条map, 为每个打开的项目的分析结果标签页分配一个滚动条 + private static final Map scrollPanes = new HashMap<>(); + + private ProjectAnalysisDataWindow(Project project) { + //初始化分析结果标签页 + JPanel window = new JPanel(new BorderLayout()); + //定义动作组 + DefaultActionGroup actionGroup = new DefaultActionGroup(); + //添加 重新执行上一次目录或文件分析 动作到动作组 + AnAction restart = new RestartLastAnalysis(); + actionGroup.add(restart); + //添加 工程分析 动作到动作组 + AnAction run = new StartProjectAnalysis(); + actionGroup.add(run); + //使用动作组创建工具栏 + ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar("ProjectAnalysisWindowToolbar", actionGroup, false); + actionToolbar.setTargetComponent(window); + + // 将工具栏添加到分析结果标签页的左侧 + window.add(actionToolbar.getComponent(), BorderLayout.WEST); + + //把滚动条添加到分析结果标签页 + JScrollPane scrollPane = new JBScrollPane(); + window.add(scrollPane, BorderLayout.CENTER); + + //把创建的window和scrollPane添加到标签页中 + windows.put(project.getBasePath(), window); + scrollPanes.put(project.getBasePath(), scrollPane); + } + + public static JComponent getContent(Project project) { + //保证每个打开的项目只创建一个分析结果标签页, 因为更新结果树时需要获取到项目的JScrollPane实例, 然后修改其内容, 所以要用单例模式(这里单例获取window修改内容也可以) + //为每个项目创建一个标签页可以防止在打开多个项目的时候出现问题 + if(!windows.containsKey(project.getBasePath())) + new ProjectAnalysisDataWindow(project); + return windows.get(project.getBasePath()); + } + //设置分析结果标签页中滚动条里的结果树, 以此来达到覆盖前一次分析结果的功能 + public static void setDataTree(Tree tree, String projectPath) { + scrollPanes.get(projectPath).setViewportView(tree); + } +} diff --git a/plugins/jetbrains_plugin/src/main/java/com/TCA/Window/ProjectAnalysisLogsWindow.java b/plugins/jetbrains_plugin/src/main/java/com/TCA/Window/ProjectAnalysisLogsWindow.java new file mode 100644 index 000000000..5bdd95325 --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/java/com/TCA/Window/ProjectAnalysisLogsWindow.java @@ -0,0 +1,96 @@ +package com.TCA.Window; + +import com.TCA.Action.*; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.ActionToolbar; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.DefaultActionGroup; +import com.intellij.openapi.project.Project; +import com.intellij.ui.JBColor; +import com.intellij.ui.components.JBScrollPane; + +import javax.swing.*; +import javax.swing.text.BadLocationException; +import javax.swing.text.Style; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyledDocument; +import java.awt.*; +import java.util.HashMap; +import java.util.Map; + +//此类用于创建分析日志标签页 +public class ProjectAnalysisLogsWindow { + //定义分析日志标签页map, 为每一个打开的项目创建一个分析日志标签页 + private static final Map windows = new HashMap<>(); + //定义分析日志文本域map, 为每一个打开的项目创建一个文本域 + private static final Map textPanes = new HashMap<>(); + + private ProjectAnalysisLogsWindow(Project project) { + //初始化分析日志标签页 + JPanel window = new JPanel(new BorderLayout()); + //定义动作组 + DefaultActionGroup actionGroup = new DefaultActionGroup(); + //添加 重新执行上一次目录或文件分析 动作到动作组 + AnAction restart = new RestartLastAnalysis(); + actionGroup.add(restart); + //添加 工程分析 动作到动作组 + AnAction run = new StartProjectAnalysis(); + actionGroup.add(run); + //添加 清除日志 动作到动作组 + AnAction clearLogs = new ClearLogs(); + actionGroup.add(clearLogs); + //使用动作组创建工具栏 + ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar("ProjectAnalysisWindowToolbar", actionGroup, false); + actionToolbar.setTargetComponent(window); + + //将工具栏添加到分析日志标签页的左侧 + window.add(actionToolbar.getComponent(), BorderLayout.WEST); + + //初始化文本域 + JTextPane textPane = new JTextPane(); + //设置不可编辑 + textPane.setEditable(false); + //添加文本域到分析滚动条 + JBScrollPane jScrollPane = new JBScrollPane(textPane, + JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + //添加滚动条分析日志标签页 + window.add(jScrollPane, BorderLayout.CENTER); + + windows.put(project.getBasePath(), window); + textPanes.put(project.getBasePath(), textPane); + } + + public static JComponent getContent(Project project) { + //保证每个打开的项目只会创建一个分析日志标签页, 因为更新文本域时需要获取到项目的JTextPane实例, 然后修改其内容, 所以要用单例模式(这里单例获取window修改内容也可以) + //为每个项目创建一个标签页可以防止在打开多个项目的时候出现问题 + if(!windows.containsKey(project.getBasePath())) + new ProjectAnalysisLogsWindow(project); + return windows.get(project.getBasePath()); + } + + //追加文本到分析日志标签页的文本域中 + public static void addText(String msg, String projectPath) { + StyledDocument styledDocument = textPanes.get(projectPath).getStyledDocument(); + Style style = textPanes.get(projectPath).addStyle("CustomStyle", null); + + // 大小 + StyleConstants.setFontSize(style, 13); + // 字体 + StyleConstants.setFontFamily(style, "JetBrains Mono"); + // 颜色 + StyleConstants.setForeground(style, JBColor.GREEN); + + try { + styledDocument.insertString(styledDocument.getLength(), msg + '\n', style); + textPanes.get(projectPath).setCaretPosition(styledDocument.getLength()); + } catch (BadLocationException e) { + throw new RuntimeException(e); + } + } + + //清除分析日志标签页中的日志 + public static void clearLogs(String projectPath) { + textPanes.get(projectPath).setText(""); + } +} \ No newline at end of file diff --git a/plugins/jetbrains_plugin/src/main/java/com/TCA/Window/ProjectMenu.java b/plugins/jetbrains_plugin/src/main/java/com/TCA/Window/ProjectMenu.java new file mode 100644 index 000000000..766ba6545 --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/java/com/TCA/Window/ProjectMenu.java @@ -0,0 +1,29 @@ +package com.TCA.Window; + +import com.intellij.openapi.project.Project; +import com.intellij.ui.components.JBTabbedPane; + +import javax.swing.*; +import java.awt.*; +//此类用于创建本地代码分析标签页 +public class ProjectMenu { + + public JBTabbedPane createMenu(Project project) { + + //创建JBTabbedPane实例, JBTabbedPane可以包含多个标签页 + JBTabbedPane tabbedPane = new JBTabbedPane(); + + //获取两个页面实例 + JPanel panel1 = new JPanel(new BorderLayout()); + panel1.add(ProjectAnalysisLogsWindow.getContent(project)); + + JPanel panel2 = new JPanel(new BorderLayout()); + panel2.add(ProjectAnalysisDataWindow.getContent(project)); + + // 将这些页面实例作为选项卡添加到 JBTabbedPane + tabbedPane.addTab("分析日志", panel1); + tabbedPane.addTab("分析结果", panel2); + + return tabbedPane; + } +} diff --git a/plugins/jetbrains_plugin/src/main/java/com/TCA/Window/TreeNodeType.java b/plugins/jetbrains_plugin/src/main/java/com/TCA/Window/TreeNodeType.java new file mode 100644 index 000000000..ad67e59a1 --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/java/com/TCA/Window/TreeNodeType.java @@ -0,0 +1,77 @@ +package com.TCA.Window; + +import com.TCA.Action.MarkCodeLine; +import com.TCA.Action.StartFileAnalysis; +import com.TCA.Data.ErrorData; +import com.TCA.Data.FileType; +import com.intellij.openapi.util.IconLoader; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreeCellRenderer; +import java.awt.*; + +//此类主要用于设置结果树中节点的展示样式, 具体方法为用label实例替代原来的节点, 以此达到修改样式的目的, 还用于删除结果树中错误数为0的目录和文件 +public class TreeNodeType implements TreeCellRenderer { + + private JLabel label; + + public TreeNodeType() { + label = new JLabel(); + label.setOpaque(true); + } + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { + //获取当前节点 + DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; + Object userObject = node.getUserObject(); + + //使用label代替原来的节点, 设置label的文本 + label.setText(userObject.toString()); + + //设置label的图标 + //判断当前节点是否是目录或文件节点 + if(userObject instanceof FileType fileType) { + //是文件或目录节点 + //根据节点是目录还是文件设置对应的图标 + if(fileType.isDirectory) { + label.setIcon(IconLoader.getIcon("/icons/directory.svg", StartFileAnalysis.class)); + } else { + label.setIcon(IconLoader.getIcon("/icons/file.svg", StartFileAnalysis.class)); + } + } + else { + //当前节点是错误信息节点 + ErrorData errorData = (ErrorData) userObject; + //根据错误等级设置图标 + if(MarkCodeLine.cmp.get(errorData.severity) > 2) + label.setIcon(IconLoader.getIcon("/icons/error.svg", StartFileAnalysis.class)); + else + label.setIcon(IconLoader.getIcon("/icons/warning.svg", StartFileAnalysis.class)); + } + return label; + } + + //DFS删除结果树中错误数为 0 的目录和文件 + public static void deleteBlankNode (DefaultMutableTreeNode node) { + //for循环遍历node的所有孩子节点 + for(int i = 0; i < node.getChildCount(); i++) { + DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) node.getChildAt(i); + Object object = childNode.getUserObject(); + if (object instanceof FileType fileType) { + //孩子节点是目录或文件节点, 就判断错误数量是否为0, 为0就删除该节点 + if (fileType.sum == 0) { + node.remove(i); + i--; + } + else { + //否则递归继续寻找错误数量为0的节点 + deleteBlankNode(childNode); + } + } else { + return; + } + } + } +} diff --git a/plugins/jetbrains_plugin/src/main/resources/META-INF/plugin.xml b/plugins/jetbrains_plugin/src/main/resources/META-INF/plugin.xml new file mode 100644 index 000000000..a411c6a52 --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,45 @@ + + org.example.TCA + + TCA + Company + + + TCA plugin + TCA plugin + TCA plugin + TCA plugin + + com.intellij.modules.platform + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/jetbrains_plugin/src/main/resources/META-INF/pluginIcon.svg b/plugins/jetbrains_plugin/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 000000000..5a9af40da --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1,20 @@ + + + logo-1280-2 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/jetbrains_plugin/src/main/resources/icons/Logo.svg b/plugins/jetbrains_plugin/src/main/resources/icons/Logo.svg new file mode 100644 index 000000000..5a9af40da --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/resources/icons/Logo.svg @@ -0,0 +1,20 @@ + + + logo-1280-2 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/jetbrains_plugin/src/main/resources/icons/canNotRun.svg b/plugins/jetbrains_plugin/src/main/resources/icons/canNotRun.svg new file mode 100644 index 000000000..dc1a7561d --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/resources/icons/canNotRun.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/jetbrains_plugin/src/main/resources/icons/clearLogs.svg b/plugins/jetbrains_plugin/src/main/resources/icons/clearLogs.svg new file mode 100644 index 000000000..7945ad0a6 --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/resources/icons/clearLogs.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/jetbrains_plugin/src/main/resources/icons/directory.svg b/plugins/jetbrains_plugin/src/main/resources/icons/directory.svg new file mode 100644 index 000000000..a01a89b62 --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/resources/icons/directory.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/jetbrains_plugin/src/main/resources/icons/error.svg b/plugins/jetbrains_plugin/src/main/resources/icons/error.svg new file mode 100644 index 000000000..4f524358e --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/resources/icons/error.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/jetbrains_plugin/src/main/resources/icons/file.svg b/plugins/jetbrains_plugin/src/main/resources/icons/file.svg new file mode 100644 index 000000000..3ebb0ee78 --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/resources/icons/file.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/jetbrains_plugin/src/main/resources/icons/run.svg b/plugins/jetbrains_plugin/src/main/resources/icons/run.svg new file mode 100644 index 000000000..ecbb07b6d --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/resources/icons/run.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/plugins/jetbrains_plugin/src/main/resources/icons/runProject.svg b/plugins/jetbrains_plugin/src/main/resources/icons/runProject.svg new file mode 100644 index 000000000..fe695fb74 --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/resources/icons/runProject.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/plugins/jetbrains_plugin/src/main/resources/icons/stop.svg b/plugins/jetbrains_plugin/src/main/resources/icons/stop.svg new file mode 100644 index 000000000..d9ee43c30 --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/resources/icons/stop.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/jetbrains_plugin/src/main/resources/icons/warning.svg b/plugins/jetbrains_plugin/src/main/resources/icons/warning.svg new file mode 100644 index 000000000..56e78170f --- /dev/null +++ b/plugins/jetbrains_plugin/src/main/resources/icons/warning.svg @@ -0,0 +1,6 @@ + + + + + +