Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

## [Unreleased]

## [0.1.0]

### Changed

- Improved exception handling and logging for better debugging and user experience.
- Changed notifications about cli not generating result files to info level logs, to reduce noise in the IDE.
- Fixed a bug where the plugin would not properly use cached resources

## [0.0.12]

### Changed
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pluginGroup = com.codacy.intellij.plugin
pluginName = codacy-intellij-plugin
pluginRepositoryUrl = https://github.com/codacy/codacy-intellij-extension
# SemVer format -> https://semver.org
pluginVersion = 0.0.12
pluginVersion = 0.1.0

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 223
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.intellij.ide.plugins.*
class PluginStateListener : DynamicPluginListener {

override fun beforePluginUnload(pluginDescriptor: IdeaPluginDescriptor, isUpdate: Boolean) {
StartupListener.cancelAllScopes()
Telemetry.track(ExtensionUnloadedEvent)

super.beforePluginUnload(pluginDescriptor, isUpdate)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ import com.codacy.intellij.plugin.services.agent.model.Provider
import com.codacy.intellij.plugin.services.api.Api
import com.codacy.intellij.plugin.services.cli.CodacyCliService
import com.codacy.intellij.plugin.services.common.Config
import com.codacy.intellij.plugin.services.common.Logger
import com.codacy.intellij.plugin.services.common.GitRemoteParser
import com.codacy.intellij.plugin.services.common.IconUtils
import com.codacy.intellij.plugin.services.git.GitProvider
import com.codacy.intellij.plugin.services.git.RepositoryManager
import com.codacy.intellij.plugin.telemetry.ExtensionInstalledEvent
import com.codacy.intellij.plugin.telemetry.Telemetry
import com.intellij.ide.plugins.DynamicPluginListener
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.startup.StartupActivity
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.BulkFileListener
Expand All @@ -26,12 +29,26 @@ class StartupListener : StartupActivity {

companion object {
private const val MIN_REFRESH_INTERVAL_MS = 5000L
private val activeScopes = java.util.Collections.synchronizedList(mutableListOf<CoroutineScope>())

fun cancelAllScopes() {
synchronized(activeScopes) {
activeScopes.forEach { it.cancel() }
activeScopes.clear()
}
}
}

private var lastTriggeredTime = 0L

@OptIn(DelicateCoroutinesApi::class)
override fun runActivity(project: Project) {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
activeScopes.add(scope)
Disposer.register(project, Disposable {
scope.cancel()
activeScopes.remove(scope)
})

// Check for first installation
val config = Config.instance
if (config.state.isFirstRun) {
Expand All @@ -50,7 +67,7 @@ class StartupListener : StartupActivity {
connection.subscribe(GitRepository.GIT_REPO_CHANGE, GitRepositoryChangeListener { repository ->


val gitResult = initializeGit(project)
val gitResult = initializeGit(project, scope)
val repositoryManager = gitResult.first
val gitInfo = gitResult.second

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initializeGit(project, scope) can throw (e.g., when no Git remote is configured). Since this call is inside the repository change listener without a try/catch, an exception here can abort the listener and spam IDE logs. Wrap this initialization in error handling and fall back to a safe state (e.g., skip starting services / set RepositoryManager to NoGitRepository).

Copilot uses AI. Check for mistakes.

Expand All @@ -60,19 +77,30 @@ class StartupListener : StartupActivity {

AiAgentService.getService(project)

GlobalScope.launch {
Api().listTools()
scope.launch {
try {
service<Api>().listTools()
} catch (e: Exception) {
Logger.warn("Failed to load tools: ${e.message}")
}
}

if (repository.currentBranch != null) {
CoroutineScope(Dispatchers.Default).launch {
scope.launch {
repositoryManager.handleStateChange()
}
} else {
repositoryManager.notifyDidChangeConfig()
}
})

// Perform initial startup initialization without waiting for a git change event
val gitRepository = GitProvider.getRepository(project)
if (gitRepository != null) {
val repositoryManager = project.service<RepositoryManager>()
scope.launch { repositoryManager.open(gitRepository) }
}

connection.subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {
private fun handle(events: List<VFileEvent>) {
val now = System.currentTimeMillis()
Expand All @@ -95,12 +123,11 @@ class StartupListener : StartupActivity {
})
}

@OptIn(DelicateCoroutinesApi::class)
private fun initializeGit(project: Project): Pair<RepositoryManager, GitRemoteParser.GitRemoteInfo> {
private fun initializeGit(project: Project, scope: CoroutineScope): Pair<RepositoryManager, GitRemoteParser.GitRemoteInfo> {
val gitRepository = GitProvider.getRepository(project)
val repositoryManager = project.service<RepositoryManager>()
if (gitRepository != null && repositoryManager.currentRepository != gitRepository)
GlobalScope.launch { repositoryManager.open(gitRepository) }
scope.launch { repositoryManager.open(gitRepository) }

val remote = gitRepository?.remotes?.firstOrNull()
?: throw IllegalStateException("No remote found in the Git repository")

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initializeGit throws IllegalStateException when no remote is found. Since this runs inside the Git repo change listener, it can surface as an unhandled exception and destabilize the plugin for repos without remotes (or during transient Git states). Handle the "no remote" case gracefully (e.g., return a sentinel result / skip initialization and update plugin state) instead of throwing here.

Copilot uses AI. Check for mistakes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL

@Service
@Service(Service.Level.APP)
class Api {

private val config = service<Config>()
Expand Down Expand Up @@ -47,15 +47,13 @@ class Api {
Gson().fromJson(response, responseClass)
}
} else {
Logger.error("Failed to fetch data: HTTP response code $responseCode")
if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED)
config.storeApiToken("")
throw Exception("Failed to fetch data: HTTP response code $responseCode")
}
Comment on lines 49 to 53

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makeRequest now throws on non-200 responses and also rethrows on any exception, removing the previous “return empty instance” fallback. Several callers (e.g., PullRequest.refresh() in services/git/PullRequest.kt) call API methods from a GlobalScope.launch without try/catch, which will now surface as unhandled coroutine exceptions and can destabilize the plugin. Consider either restoring a safe fallback per endpoint, or auditing call sites to handle exceptions explicitly (and updating UI/state accordingly).

Copilot uses AI. Check for mistakes.
}
catch (e: Exception) {
Comment thread
lventura-codacy marked this conversation as resolved.
Logger.error("Failed to fetch data: ${e.message}")
responseClass.newInstance()
throw Exception("Failed to fetch data: ${e.message}", e)
}
Comment thread
lventura-codacy marked this conversation as resolved.
}
}
Expand Down Expand Up @@ -131,7 +129,7 @@ class Api {
return try {
makeRequest(endpointUrl, GetUserProfileResponse::class.java).data
} catch (e: Exception) {
Logger.error("Failed to fetch user profile: ${e.message}")
Logger.warn("Failed to fetch user profile: ${e.message}")
null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.codacy.intellij.plugin.services.common.Config.Companion.CODACY_TOOLS_
import com.codacy.intellij.plugin.services.common.Config.Companion.CODACY_YAML_NAME
import com.codacy.intellij.plugin.services.common.GitRemoteParser
import com.codacy.intellij.plugin.services.common.IconUtils
import com.codacy.intellij.plugin.services.common.Logger
import com.codacy.intellij.plugin.services.common.OsType
import com.codacy.intellij.plugin.services.common.SystemDetectionService
import com.codacy.intellij.plugin.services.git.GitProvider
Expand Down Expand Up @@ -492,16 +493,11 @@ class CodacyCliService() {
}

if (fileOutput.isFailure) {
notificationManager.createNotification(
"CLI tools has not generated a result file",
"Current document will not be analyzed, please try again",
NotificationType.ERROR
).notify(project)
return@withTempFile
Logger.info("No analysis result file found for file: $file. Is it a supported file type?")
}

results = fileOutput.getOrNull()
.let { SarifUtil.readReport(StringReader(it)).runs }
?.let { SarifUtil.readReport(StringReader(it)).runs }
?.let(::processSarifResults)
?: emptyList()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import com.codacy.intellij.plugin.telemetry.Telemetry
import com.intellij.credentialStore.CredentialAttributes
import com.intellij.credentialStore.Credentials
import com.intellij.ide.passwordSafe.PasswordSafe
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.*
import com.intellij.util.SlowOperations
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.util.*

Expand All @@ -18,10 +21,11 @@ import java.util.*
storages = [Storage("CodacyPluginSettings.xml")]
)
@Service
class Config : PersistentStateComponent<Config.State> {
class Config : PersistentStateComponent<Config.State>, Disposable {

private var state = State()
private var apiToken: String? = null
private val serviceScope = CoroutineScope(Dispatchers.Default + SupervisorJob())

companion object {
const val CODACY_DIRECTORY_NAME: String = ".codacy"
Expand Down Expand Up @@ -78,7 +82,6 @@ class Config : PersistentStateComponent<Config.State> {
}
}

@OptIn(DelicateCoroutinesApi::class)
fun storeApiToken(token: String?) {
val credentialAttributes = createCredentialAttributes()
if (token.isNullOrBlank()) {
Expand All @@ -92,8 +95,8 @@ class Config : PersistentStateComponent<Config.State> {
log.info("API Token stored")

if (state.userId == null) {
GlobalScope.launch {
val userProfile = Api().getUserProfile()
serviceScope.launch {
val userProfile = service<Api>().getUserProfile()

if (userProfile?.id != null && state.userId == null) {
state.userId = userProfile.id
Expand Down Expand Up @@ -122,4 +125,8 @@ class Config : PersistentStateComponent<Config.State> {

private fun createCredentialAttributes(): CredentialAttributes =
CredentialAttributes("Codacy", "")

override fun dispose() {
serviceScope.cancel()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.codacy.intellij.plugin.services.git.RepositoryManager.RepositoryManag
import kotlinx.coroutines.*
import com.intellij.notification.*
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
Expand All @@ -32,7 +33,7 @@ class PullRequest(
var files: List<FileDetails> = mutableListOf()
var gates: QualitySettingsData? = null
private val refreshTimeout: TimeoutManager = TimeoutManager()
private val api = Api()
private val api = service<Api>()
private var repositoryManager: RepositoryManager? = null
Comment on lines 35 to 37

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Api calls can now throw (via makeRequest), but PullRequest.refresh() / fetch*() paths currently don't have a top-level try/catch around the sequence of API calls. A single transient failure can abort the refresh coroutine and stop subsequent scheduled refreshes. Add appropriate exception handling around the refresh flow (and consider updating the state/notification) so refresh is resilient to network/API errors.

Copilot uses AI. Check for mistakes.

fun init(
Expand Down
Loading
Loading