diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3fba5ba..7eecef1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -168,7 +168,7 @@ jobs: # Run Qodana inspections - name: Qodana - Code Inspection - uses: JetBrains/qodana-action@v2024.1.2 + uses: JetBrains/qodana-action@v2024.1 with: cache-default-branch-only: true diff --git a/.gitignore b/.gitignore index 654ce68..c5db87b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ -.gradle -.idea +.gradle/ +.idea/* +!.idea/icon.svg .qodana assets -build +build/ .DS_Store diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100644 index 0000000..092b0bc --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index ceb97b3..e725df3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,20 @@ ## [Unreleased] +### Added + +- Introduce a setting to customize the maximum amount of simultaneous scans + +### Changed + +- Slightly improve performance on scans startup +- Logic improvements for scanners + +### Fixed + +- Fix a rare crash with the status bar when navigating between projects (#99) +- Fix cached updates not being used when they should + ## [2.2.1] - 2024-04-26 ### Changed diff --git a/gradle.properties b/gradle.properties index 983f802..cae3d05 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = com.github.warningimhack3r.npmupdatedependencies pluginName = npm-update-dependencies pluginRepositoryUrl = https://github.com/WarningImHack3r/npm-update-dependencies # SemVer format -> https://semver.org -pluginVersion = 2.2.1 +pluginVersion = 2.3.0 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 221 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e720888..bd24c92 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ semver4j = "5.3.0" kotlin = "1.9.23" changelog = "2.2.0" gradleIntelliJPlugin = "1.17.3" -qodana = "2024.1.2" +qodana = "2024.1.3" kover = "0.7.6" [libraries] diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/NPMJSClient.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/NPMJSClient.kt index ebe320e..414407a 100644 --- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/NPMJSClient.kt +++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/NPMJSClient.kt @@ -10,7 +10,6 @@ import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonObject import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest @@ -79,12 +78,7 @@ class NPMJSClient(private val project: Project) { log.warn("Error while getting response body from $uri", e) return null } - return try { - Json.parseToJsonElement(responseBody).jsonObject - } catch (e: Exception) { - log.warn("Error while parsing response body from $uri", e) - null - } + return Json.parseToJsonElement(responseBody).asJsonObject } fun getLatestVersion(packageName: String): String? { @@ -112,7 +106,8 @@ class NPMJSClient(private val project: Project) { val json = getBodyAsJSON("${registry}/$packageName") return json?.get("versions")?.asJsonObject?.keys?.toList().also { if (it != null) { - log.info("All versions for package $packageName found in cache: $it") + log.info("All versions for package $packageName found in cache (${it.size} versions)") + log.debug("Versions in cache for $packageName: $it") } } ?: ShellRunner.execute( arrayOf("npm", "v", packageName, "versions", "--json", "--registry=$registry") @@ -132,7 +127,8 @@ class NPMJSClient(private val project: Project) { } }.also { versions -> if (versions != null) { - log.info("All versions for package $packageName found: $versions") + log.info("All versions for package $packageName found (${versions.size} versions)") + log.debug("Versions for $packageName: $versions") } } } diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/PackageUpdateChecker.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/PackageUpdateChecker.kt index 2cf3c5d..82bd460 100644 --- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/PackageUpdateChecker.kt +++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/PackageUpdateChecker.kt @@ -41,13 +41,10 @@ class PackageUpdateChecker(private val project: Project) { } private fun areVersionsMatchingComparatorNeeds(versions: Versions, comparator: String): Boolean { - return if (versions.latest.satisfies(comparator)) { - versions.satisfies == null - } else { - versions.satisfies != null - && versions.satisfies.satisfies(comparator) - && isVersionMoreRecentThanComparator(versions.satisfies, comparator) - } && isVersionMoreRecentThanComparator(versions.latest, comparator) + return isVersionMoreRecentThanComparator(versions.latest, comparator) && versions.satisfies?.let { satisfying -> + satisfying.satisfies(comparator) + && isVersionMoreRecentThanComparator(satisfying, comparator) + } ?: true } private fun getVersionExcludingFilter(packageName: String, version: Semver): String? { @@ -81,6 +78,9 @@ class PackageUpdateChecker(private val project: Project) { if (areVersionsMatchingComparatorNeeds(cachedVersions.versions, comparator)) { log.info("Cached versions for $packageName are still valid, returning them") return cachedVersions + } else { + log.debug("Cached versions for $packageName are outdated, removing them") + availableUpdates.remove(packageName) } } @@ -94,7 +94,7 @@ class PackageUpdateChecker(private val project: Project) { var satisfyingVersion: Semver? = null val updateAvailable = isVersionMoreRecentThanComparator(newestVersion, comparator) if (!updateAvailable) { - if (availableUpdates.containsKey(packageName)) { + availableUpdates[packageName]?.let { availableUpdates.remove(packageName) } log.info("No update available for $packageName, removing cached versions") diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/RegistriesScanner.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/RegistriesScanner.kt index 7950381..f2b34dd 100644 --- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/RegistriesScanner.kt +++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/backend/engine/RegistriesScanner.kt @@ -6,7 +6,7 @@ import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project @Service(Service.Level.PROJECT) -class RegistriesScanner { +class RegistriesScanner(private val project: Project) { companion object { private val log = logger() @@ -17,7 +17,9 @@ class RegistriesScanner { var registries: List = emptyList() fun scan() { + val state = NUDState.getInstance(project) log.info("Starting to scan registries") + state.isScanningForRegistries = true // Run `npm config ls` to get the list of registries val config = ShellRunner.execute(arrayOf("npm", "config", "ls")) ?: return registries = config.lines().asSequence().filter { line -> @@ -36,5 +38,6 @@ class RegistriesScanner { } }.map { it.removeSuffix("/") }.distinct().toList() log.info("Found registries: $registries") + state.isScanningForRegistries = false } } diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/settings/NUDSettingsComponent.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/settings/NUDSettingsComponent.kt index 49b8208..2760f56 100644 --- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/settings/NUDSettingsComponent.kt +++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/settings/NUDSettingsComponent.kt @@ -6,6 +6,7 @@ import com.github.warningimhack3r.npmupdatedependencies.backend.engine.NUDState import com.github.warningimhack3r.npmupdatedependencies.ui.statusbar.StatusBarMode import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer import com.intellij.ide.DataManager +import com.intellij.openapi.application.ApplicationNamesInfo import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.runInEdt import com.intellij.openapi.fileEditor.FileEditorManager @@ -122,7 +123,7 @@ class NUDSettingsComponent { setCancelOperation { dialogWrapper.close(DialogWrapper.CANCEL_EXIT_CODE) } - dialogWrapper.setSize(400, 300) // Width is not really respected, text width takes over + dialogWrapper.setSize(400, 300) // Width is not respected, text width takes over } val panel = panel { @@ -149,6 +150,19 @@ class NUDSettingsComponent { .bindSelected(settings::autoReorderDependencies) } } + group("Parallelism") { + row("Maximum parallel processes:") { + spinner(1..100) + .comment( + "Control the maximum number of parallel scans that can be run at the same time. Higher values can speed up the scan but might cause performance issues or out of memory issues. Make sure to bump ${ + ApplicationNamesInfo.getInstance().fullProductName.substringBefore( + " " + ) + }'s memory as needed.
100 means no limit." + ) + .bindIntValue(settings::maxParallelism) + } + } group("Status Bar") { lateinit var statusBarEnabled: Cell row { diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/settings/NUDSettingsState.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/settings/NUDSettingsState.kt index 9d16b18..f5781dc 100644 --- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/settings/NUDSettingsState.kt +++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/settings/NUDSettingsState.kt @@ -45,6 +45,11 @@ class NUDSettingsState : PersistentStateComponent { set(value) { settings.autoReorderDependencies = value } + var maxParallelism: Int + get() = settings.maxParallelism + set(value) { + settings.maxParallelism = value + } var showStatusBarWidget: Boolean get() = settings.showStatusBarWidget set(value) { @@ -71,6 +76,7 @@ class NUDSettingsState : PersistentStateComponent { var defaultDeprecationAction: Deprecation.Action = Deprecation.Action.REPLACE, var showDeprecationBanner: Boolean = true, var autoReorderDependencies: Boolean = true, + var maxParallelism: Int = 100, var showStatusBarWidget: Boolean = true, var statusBarMode: StatusBarMode = StatusBarMode.FULL, var autoFixOnSave: Boolean = false, diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/DeprecationAnnotator.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/DeprecationAnnotator.kt index 0d9294a..2f0ce4b 100644 --- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/DeprecationAnnotator.kt +++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/DeprecationAnnotator.kt @@ -6,6 +6,7 @@ import com.github.warningimhack3r.npmupdatedependencies.backend.engine.NPMJSClie import com.github.warningimhack3r.npmupdatedependencies.backend.engine.NUDState import com.github.warningimhack3r.npmupdatedependencies.backend.engine.RegistriesScanner import com.github.warningimhack3r.npmupdatedependencies.backend.extensions.parallelMap +import com.github.warningimhack3r.npmupdatedependencies.settings.NUDSettingsState import com.github.warningimhack3r.npmupdatedependencies.ui.helpers.AnnotatorsCommon import com.github.warningimhack3r.npmupdatedependencies.ui.quickfix.DeprecatedDependencyFix import com.intellij.codeInspection.ProblemHighlightType @@ -19,6 +20,7 @@ import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile import com.intellij.ui.EditorNotifications import com.intellij.util.applyIf +import kotlinx.coroutines.delay class DeprecationAnnotator : DumbAware, ExternalAnnotator< Pair>, @@ -37,21 +39,24 @@ class DeprecationAnnotator : DumbAware, ExternalAnnotator< var state = NUDState.getInstance(project) if (!state.isScanningForRegistries && state.packageRegistries.isEmpty()) { - state.isScanningForRegistries = true log.debug("No registries found, scanning for registries...") RegistriesScanner.getInstance(project).scan() log.debug("Registries scanned") - state.isScanningForRegistries = false } - while (state.isScanningForRegistries || state.isScanningForDeprecations) { - // Wait for the registries to be scanned and avoid multiple scans at the same time - log.debug("Waiting for registries to be scanned...") + if (state.isScanningForRegistries || state.isScanningForUpdates) { + log.debug("Waiting for registries and/or updates to be scanned...") + while (state.isScanningForRegistries || state.isScanningForUpdates) { + // Wait for the registries to be scanned and avoid multiple scans at the same time + } } log.debug("Scanning for deprecations...") state = NUDState.getInstance(project) + val maxParallelism = NUDSettingsState.instance.maxParallelism + var activeTasks = 0 val npmjsClient = NPMJSClient.getInstance(project) + return info .also { // Remove from the cache all deprecations that are no longer in the file @@ -62,8 +67,18 @@ class DeprecationAnnotator : DumbAware, ExternalAnnotator< state.scannedDeprecations = 0 state.isScanningForDeprecations = true }.parallelMap { property -> + if (maxParallelism < 100) { + while (activeTasks >= maxParallelism) { + // Wait for the active tasks count to decrease + delay(50) + } + activeTasks++ + log.debug("Task $activeTasks/$maxParallelism started: ${property.name}") + } state.deprecations[property.name]?.let { deprecation -> + log.debug("Deprecation found in cache: ${property.name}") state.scannedDeprecations++ + activeTasks-- // If the deprecation is already in the cache, we don't need to check the NPM registry Pair(property.jsonProperty, deprecation) } ?: npmjsClient.getPackageDeprecation(property.name)?.let { reason -> @@ -72,45 +87,44 @@ class DeprecationAnnotator : DumbAware, ExternalAnnotator< // Remove punctuation at the end of the word word.replace(Regex("[,;.]$"), "") }.filter { word -> - // Try to find a word that looks like a package name - if (word.startsWith("@")) { - // Scoped package - return@filter word.split("/").size == 2 - } - if (word.contains("/")) { - // If it contains a slash without being a scoped package, it's likely an URL - return@filter false + with(word) { + // Try to find a word that looks like a package name + when { + // Scoped package + startsWith("@") -> split("/").size == 2 + // If it contains a slash without being a scoped package, it's likely an URL + contains("/") -> false + // Other potential matches + contains("-") -> lowercase() == this + // Else if we're unsure, we don't consider it as a package name + else -> false + } } - // Other potential matches - if (word.contains("-")) { - return@filter word.lowercase() == word - } - // Else if we're unsure, we don't consider it as a package name - false }.parallelMap innerMap@{ potentialPackage -> // Confirm that the word is a package name by trying to get its latest version npmjsClient.getLatestVersion(potentialPackage)?.let { Pair(potentialPackage, it) } - }.filterNotNull().also { - state.scannedDeprecations++ - }.firstOrNull()?.let { (name, version) -> + }.filterNotNull().firstOrNull()?.let { (name, version) -> // We found a package name and its latest version, so we can create a replacement Pair(property.jsonProperty, Deprecation(reason, Deprecation.Replacement(name, version))) } ?: Pair( property.jsonProperty, Deprecation(reason, null) ) // No replacement found in the deprecation reason - }.also { pair -> - pair?.let { + }.also { result -> + result?.let { (property, deprecation) -> // Add the deprecation to the cache if any - state.deprecations[property.name] = it.second - } ?: run { + state.deprecations[property.name] = deprecation + } ?: state.deprecations[property.name]?.let { _ -> // Remove the deprecation from the cache if no deprecation is found - if (state.deprecations.containsKey(property.name)) { - state.deprecations.remove(property.name) - } + state.deprecations.remove(property.name) } + + log.debug("Finished task for ${property.name}, deprecation found: ${result != null}") + // Manage counters + state.scannedDeprecations++ + activeTasks-- } }.filterNotNull().toMap().also { log.debug("Deprecations scanned, ${it.size} found") diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/UpdatesAnnotator.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/UpdatesAnnotator.kt index 3bdcbc4..2581578 100644 --- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/UpdatesAnnotator.kt +++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/annotation/UpdatesAnnotator.kt @@ -8,6 +8,7 @@ import com.github.warningimhack3r.npmupdatedependencies.backend.engine.PackageUp import com.github.warningimhack3r.npmupdatedependencies.backend.engine.RegistriesScanner import com.github.warningimhack3r.npmupdatedependencies.backend.extensions.parallelMap import com.github.warningimhack3r.npmupdatedependencies.backend.extensions.stringValue +import com.github.warningimhack3r.npmupdatedependencies.settings.NUDSettingsState import com.github.warningimhack3r.npmupdatedependencies.ui.helpers.AnnotatorsCommon import com.github.warningimhack3r.npmupdatedependencies.ui.quickfix.BlacklistVersionFix import com.github.warningimhack3r.npmupdatedependencies.ui.quickfix.UpdateDependencyFix @@ -21,6 +22,7 @@ import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile import com.intellij.util.applyIf +import kotlinx.coroutines.delay import org.semver4j.Semver class UpdatesAnnotator : DumbAware, ExternalAnnotator< @@ -40,21 +42,24 @@ class UpdatesAnnotator : DumbAware, ExternalAnnotator< var state = NUDState.getInstance(project) if (!state.isScanningForRegistries && state.packageRegistries.isEmpty()) { - state.isScanningForRegistries = true log.debug("No registries found, scanning for registries...") RegistriesScanner.getInstance(project).scan() log.debug("Registries scanned") - state.isScanningForRegistries = false } - while (state.isScanningForRegistries || state.isScanningForUpdates) { - // Wait for the registries to be scanned and avoid multiple scans at the same time - log.debug("Waiting for registries to be scanned...") + if (state.isScanningForRegistries || state.isScanningForDeprecations) { + log.debug("Waiting for registries and/or deprecations to be scanned...") + while (state.isScanningForRegistries || state.isScanningForDeprecations) { + // Wait for the registries to be scanned and avoid multiple scans at the same time + } } log.debug("Scanning for updates...") state = NUDState.getInstance(project) + val maxParallelism = NUDSettingsState.instance.maxParallelism + var activeTasks = 0 val updateChecker = PackageUpdateChecker.getInstance(project) + return info .also { // Remove from the cache all properties that are no longer in the file @@ -65,11 +70,24 @@ class UpdatesAnnotator : DumbAware, ExternalAnnotator< state.scannedUpdates = 0 state.isScanningForUpdates = true }.parallelMap { property -> - val value = property.comparator ?: return@parallelMap null + if (maxParallelism < 100) { + while (activeTasks >= maxParallelism) { + // Wait for the active tasks count to decrease + delay(50) + } + activeTasks++ + log.debug("Task $activeTasks/$maxParallelism started: ${property.name}") + } + val value = property.comparator ?: return@parallelMap null.also { + log.debug("Empty comparator for ${property.name}, skipping") + activeTasks-- + } val scanResult = updateChecker.areUpdatesAvailable(property.name, value) state.scannedUpdates++ val coerced = Semver.coerce(value) + log.debug("Task finished for ${property.name}") + activeTasks-- if (scanResult != null && coerced != null && !scanResult.versions.isEqualToAny(coerced)) { Pair(property.jsonProperty, scanResult) } else null diff --git a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/statusbar/StatusBarHelper.kt b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/statusbar/StatusBarHelper.kt index f1784bd..4957d0a 100644 --- a/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/statusbar/StatusBarHelper.kt +++ b/src/main/kotlin/com/github/warningimhack3r/npmupdatedependencies/ui/statusbar/StatusBarHelper.kt @@ -13,7 +13,7 @@ object StatusBarHelper { for (project in projectManager.openProjects) { log.debug("Updating widget for project ${project.name}") val widgetBar = - WindowManager.getInstance().getStatusBar(project).getWidget(WidgetBar.ID) as? WidgetBar ?: continue + WindowManager.getInstance().getStatusBar(project)?.getWidget(WidgetBar.ID) as? WidgetBar ?: continue widgetBar.update() log.debug("Widget updated for project ${project.name}") }