From 526d8c909494be13e85d72ce24fac327af689010 Mon Sep 17 00:00:00 2001 From: Ashish Reddy Podduturi Date: Tue, 14 Jan 2025 09:39:55 -0800 Subject: [PATCH 1/8] Fix for Test generation zip to include target file, Regex pattern fix and fix for /test query for non-supported languages. --- ...-f47212d1-8e2b-4c82-9e0d-450ce896e9bc.json | 4 ++ .../controller/CodeTestChatController.kt | 2 +- .../sessionconfig/CodeTestSessionConfig.kt | 19 +++++- .../amazonq/FeatureDevSessionContext.kt | 66 ++++++++++++------- 4 files changed, 63 insertions(+), 28 deletions(-) create mode 100644 .changes/next-release/bugfix-f47212d1-8e2b-4c82-9e0d-450ce896e9bc.json diff --git a/.changes/next-release/bugfix-f47212d1-8e2b-4c82-9e0d-450ce896e9bc.json b/.changes/next-release/bugfix-f47212d1-8e2b-4c82-9e0d-450ce896e9bc.json new file mode 100644 index 00000000000..83cfbccbecb --- /dev/null +++ b/.changes/next-release/bugfix-f47212d1-8e2b-4c82-9e0d-450ce896e9bc.json @@ -0,0 +1,4 @@ +{ + "type" : "bugfix", + "description" : "Amazon Q /test: Fix for test generation payload creation to not filter out target file." +} \ No newline at end of file diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt index b7cb65358d6..9e738a2d1e5 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt @@ -278,7 +278,7 @@ class CodeTestChatController( val requestData = ChatRequestData( tabId = session.tabId, - message = "Generate unit tests for the following part of my code: ${message.prompt}", + message = "Generate unit tests for the following part of my code: ${message.prompt.ifBlank { fileInfo.fileName }}", activeFileContext = activeFileContext, userIntent = UserIntent.GENERATE_UNIT_TESTS, triggerType = TriggerType.ContextMenu, diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codetest/sessionconfig/CodeTestSessionConfig.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codetest/sessionconfig/CodeTestSessionConfig.kt index fcd2fbfcabe..92aee8fff63 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codetest/sessionconfig/CodeTestSessionConfig.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codetest/sessionconfig/CodeTestSessionConfig.kt @@ -140,7 +140,7 @@ class CodeTestSessionConfig( } // 2. Add the "utgRequiredArtifactsDir" directory - val utgDir = "utgRequiredArtifactsDir" + val utgDir = "utgRequiredArtifactsDir/" // Note the trailing slash which adds it as a directory and not a file LOG.debug { "Adding directory to ZIP: $utgDir" } val utgEntry = ZipEntry(utgDir) it.putNextEntry(utgEntry) @@ -149,7 +149,7 @@ class CodeTestSessionConfig( val buildAndExecuteLogDir = "buildAndExecuteLogDir" val subDirs = listOf(buildAndExecuteLogDir, "repoMapData", "testCoverageDir") subDirs.forEach { subDir -> - val subDirPathString = Path.of(utgDir, subDir).name + val subDirPathString = Path.of(utgDir, subDir).toString() + "/" // Added trailing slash similar to utgRequiredArtifactsDir LOG.debug { "Adding empty directory to ZIP: $subDirPathString" } val zipEntry = ZipEntry(subDirPathString) it.putNextEntry(zipEntry) @@ -167,6 +167,18 @@ class CodeTestSessionConfig( var currentTotalLines = 0L val languageCounts = mutableMapOf() + // Adding Target File to make sure target file doesn't get filtered out. + selectedFile?.let { selected -> + files.add(selected.path) + currentTotalFileSize += selected.length + currentTotalLines += countLinesInVirtualFile(selected) + selected.programmingLanguage().let { language -> + if (language !is CodeWhispererUnknownLanguage) { + languageCounts[language] = (languageCounts[language] ?: 0) + 1 + } + } + } + moduleLoop@ for (module in project.modules) { val changeListManager = ChangeListManager.getInstance(module.project) if (module.guessModuleDir() != null) { @@ -175,7 +187,8 @@ class CodeTestSessionConfig( val current = stack.pop() if (!current.isDirectory) { - if (current.isFile && !changeListManager.isIgnoredFile(current) && + if (current.isFile && current.path != selectedFile?.path && + !changeListManager.isIgnoredFile(current) && runBlocking { !featureDevSessionContext.ignoreFile(current) } && runReadAction { !fileIndex.isInLibrarySource(current) } ) { diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt index 214b837bd93..beb907b40c8 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt @@ -44,28 +44,16 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo private val ignorePatterns = setOf( "\\.aws-sam", "\\.svn", - "\\.hg/?", - "\\.rvm", - "\\.git/?", - "\\.gitignore", - "\\.project", - "\\.gem", - "/\\.idea/?", - "\\.zip$", - "\\.bin$", - "\\.png$", - "\\.jpg$", - "\\.svg$", - "\\.pyc$", - "/license\\.txt$", - "/License\\.txt$", - "/LICENSE\\.txt$", - "/license\\.md$", - "/License\\.md$", - "/LICENSE\\.md$", - "node_modules/?", - "build/?", - "dist/?" + "/\\.(?:hg|git)/?", // Combine version control system patterns + "\\.(?:rvm|gem)", // Combine Ruby-related patterns + "/?\\.(?:gitignore|project|idea/?)", // Combine project config patterns + "\\.(?:zip|bin)$", // Combine binary file patterns + "\\.(?:png|jpg|svg)$", // Combine image file patterns + "\\.pyc$", // Python compiled files + "/[Ll][Ii][Cc][Ee][Nn][Ss][Ee]\\.(txt|md)$", // Case-insensitive license files + "(?:^|.*/?)node_modules(?:/.*)?\$", + "(?:^|.*/?)build(?:/.*)?\$", + "(?:^|.*/?)dist(?:/.*)?\$" ).map { Regex(it) } // well known source files that do not have extensions @@ -218,9 +206,39 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo // gitignore patterns are not regex, method update needed. private fun convertGitIgnorePatternToRegex(pattern: String): String = pattern + // Escape special regex characters except * and ? .replace(".", "\\.") - .replace("*", ".*") - .let { if (it.endsWith("/")) "$it?" else it } // Handle directory-specific patterns by optionally matching trailing slash + .replace("+", "\\+") + .replace("(", "\\(") + .replace(")", "\\)") + .replace("[", "\\[") + .replace("]", "\\]") + .replace("{", "\\{") + .replace("}", "\\}") + .replace(",", "\\,") + .replace("^", "\\^") + .replace("$", "\\$") + .replace("|", "\\|") + // Convert gitignore glob patterns to regex + .replace("**", ".*?") // Match any directory depth + .replace("*", "[^/]*?") // Match any character except path separator + .replace("?", "[^/]") // Match single character except path separator + .let { pattern -> + when { + // If pattern starts with '/', anchor it to the start of the path + pattern.startsWith("/") -> "^${pattern.substring(1)}" + // If pattern doesn't start with '/', it can match anywhere in the path + else -> "(?:^|.*/?)$pattern" + } + } + .let { pattern -> + when { + // If pattern ends with '/', it should match directories + pattern.endsWith("/") -> "$pattern.*" + // Otherwise match exactly or with a trailing slash for directories + else -> "$pattern(?:/.*)?$" + } + } var selectedSourceFolder: VirtualFile set(newRoot) { From a394c851367de4dc78814304698bf9d821d329f6 Mon Sep 17 00:00:00 2001 From: Ashish Reddy Podduturi Date: Wed, 15 Jan 2025 12:15:15 -0800 Subject: [PATCH 2/8] Removing FeatureDevSessionContext changes. --- .../FeatureDevSessionContextTest.kt | 81 ++++++++++++++++++- .../amazonq/FeatureDevSessionContext.kt | 45 +++++++---- 2 files changed, 108 insertions(+), 18 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt index 838066a1560..fba210acd32 100644 --- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt +++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt @@ -3,6 +3,7 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.testFramework.RuleChain +import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before @@ -75,6 +76,7 @@ class FeatureDevSessionContextTest : FeatureDevTestBase(HeavyJavaCodeInsightTest @Test fun testZipProject() { addFilesToProjectModule( + ".gitignore", ".gradle/cached.jar", "src/MyClass.java", "gradlew", @@ -83,6 +85,19 @@ class FeatureDevSessionContextTest : FeatureDevTestBase(HeavyJavaCodeInsightTest "settings.gradle", "build.gradle", "gradle/wrapper/gradle-wrapper.properties", + "builder/GetTestBuilder.java", + ".aws-sam/build/function1", + ".gem/specs.rb", + "archive.zip", + "output.bin", + "images/logo.png", + "assets/header.jpg", + "icons/menu.svg", + "license.txt", + "License.md", + "node_modules/express", + "build/outputs", + "dist/bundle.js" ) val zipResult = featureDevSessionContext.getProjectZip() @@ -102,11 +117,73 @@ class FeatureDevSessionContextTest : FeatureDevTestBase(HeavyJavaCodeInsightTest "gradlew", "gradlew.bat", "README.md", - "settings.gradle", - "build.gradle", "gradle/wrapper/gradle-wrapper.properties", + "builder/GetTestBuilder.java" ) assertTrue(zippedFiles == expectedFiles) } + + @Test + fun `test basic pattern conversion`() { + val input = "*.txt" + val expected = "(?:^|.*/?)[^/]*[^/]\\.txt(?:/.*)?\$" + assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) + } + + @Test + fun `test pattern with special characters`() { + val input = "test[abc].txt" + val expected = "(?:^|.*/?)test\\[abc\\]\\.txt(?:/.*)?$" + assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) + } + + @Test + fun `test pattern with double asterisk`() { + val input = "**/build" + val expected = "(?:^|.*/?).[^/]*[^/][^/]/build(?:/.*)?\$" + assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) + } + + @Test + fun `test pattern starting with slash`() { + val input = "/root/file.txt" + val expected = "^root/file\\.txt(?:/.*)?$" + assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) + } + + @Test + fun `test pattern ending with slash`() { + val input = "build/" + val expected = "(?:^|.*/?)build/.*" + assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) + } + + @Test + fun `test pattern with question mark`() { + val input = "file?.txt" + val expected = "(?:^|.*/?)file[^/]\\.txt(?:/.*)?$" + assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) + } + + @Test + fun `test complex pattern with multiple special characters`() { + val input = "**/test-[0-9]*.{java,kt}" + val expected = "(?:^|.*/?).[^/]*[^/][^/]/test-\\[0-9\\][^/]*[^/]\\.\\{java\\,kt\\}(?:/.*)?\$" + assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) + } + + @Test + fun `test empty pattern`() { + val input = "" + val expected = "(?:^|.*/?)(?:/.*)?$" + assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) + } + + @Test + fun `test pattern with all special regex characters`() { + val input = ".$+()[]{}^|" + val expected = "(?:^|.*/?)\\.\\\$\\+\\(\\)\\[\\]\\{\\}\\^\\|(?:/.*)?$" + assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) + } } diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt index 09df18304cc..a2bdcd2cddf 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt @@ -45,21 +45,34 @@ class RepoSizeLimitError(override val message: String) : RuntimeException(), Rep class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Long? = null) { // TODO: Need to correct this class location in the modules going further to support both amazonq and codescan. - + private val additionalGitIgnoreRules = setOf( - "\\.aws-sam", - "\\.svn", - "/\\.(?:hg|git)/?", // Combine version control system patterns - "\\.(?:rvm|gem)", // Combine Ruby-related patterns - "/?\\.(?:gitignore|project|idea/?)", // Combine project config patterns - "\\.(?:zip|bin)$", // Combine binary file patterns - "\\.(?:png|jpg|svg)$", // Combine image file patterns - "\\.pyc$", // Python compiled files - "/[Ll][Ii][Cc][Ee][Nn][Ss][Ee]\\.(txt|md)$", // Case-insensitive license files - "(?:^|.*/?)node_modules(?:/.*)?\$", - "(?:^|.*/?)build(?:/.*)?\$", - "(?:^|.*/?)dist(?:/.*)?\$" - ).map { Regex(it) } + ".aws-sam", + ".gem", + ".git", + ".gitignore", + ".gradle", + ".hg", + ".idea", + ".project", + ".rvm", + ".svn", + "*.zip", + "*.bin", + "*.png", + "*.jpg", + "*.svg", + "*.pyc", + "license.txt", + "License.txt", + "LICENSE.txt", + "license.md", + "License.md", + "LICENSE.md", + "node_modules", + "build", + "dist" + ) // well known source files that do not have extensions private val wellKnownSourceFiles = setOf( @@ -81,7 +94,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo init { ignorePatternsWithGitIgnore = try { buildList { - addAll(additionalGitIgnoreRules) + addAll(additionalGitIgnoreRules.map { convertGitIgnorePatternToRegex(it) }) addAll(parseGitIgnore()) }.mapNotNull { pattern -> runCatching { Regex(pattern) }.getOrNull() @@ -239,7 +252,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo } // gitignore patterns are not regex, method update needed. - private fun convertGitIgnorePatternToRegex(pattern: String): String = pattern + fun convertGitIgnorePatternToRegex(pattern: String): String = pattern // Escape special regex characters except * and ? .replace(".", "\\.") .replace("+", "\\+") From e7544f0b58f42bf080a4e1256407fa0436768189 Mon Sep 17 00:00:00 2001 From: Ashish Reddy Podduturi Date: Wed, 15 Jan 2025 12:29:24 -0800 Subject: [PATCH 3/8] Added comment explaining file inclusion. --- .../services/amazonqFeatureDev/FeatureDevSessionContextTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt index fba210acd32..3ba1bad2221 100644 --- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt +++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt @@ -85,7 +85,7 @@ class FeatureDevSessionContextTest : FeatureDevTestBase(HeavyJavaCodeInsightTest "settings.gradle", "build.gradle", "gradle/wrapper/gradle-wrapper.properties", - "builder/GetTestBuilder.java", + "builder/GetTestBuilder.java", //check for false positives ".aws-sam/build/function1", ".gem/specs.rb", "archive.zip", From 736516206c54f65b68925b9ff9f1c11746c17b90 Mon Sep 17 00:00:00 2001 From: Ashish Reddy Podduturi Date: Wed, 15 Jan 2025 13:52:36 -0800 Subject: [PATCH 4/8] Added Timeout and length check for pattern matching --- .../FeatureDevSessionContextTest.kt | 2 +- .../amazonq/FeatureDevSessionContext.kt | 88 +++++++++++-------- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt index 3ba1bad2221..0b4a84dfe47 100644 --- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt +++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt @@ -85,7 +85,7 @@ class FeatureDevSessionContextTest : FeatureDevTestBase(HeavyJavaCodeInsightTest "settings.gradle", "build.gradle", "gradle/wrapper/gradle-wrapper.properties", - "builder/GetTestBuilder.java", //check for false positives + "builder/GetTestBuilder.java", // check for false positives ".aws-sam/build/function1", ".gem/specs.rb", "archive.zip", diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt index a2bdcd2cddf..78d89442588 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout import org.apache.commons.codec.digest.DigestUtils import org.apache.commons.io.FileUtils import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext @@ -97,7 +98,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo addAll(additionalGitIgnoreRules.map { convertGitIgnorePatternToRegex(it) }) addAll(parseGitIgnore()) }.mapNotNull { pattern -> - runCatching { Regex(pattern) }.getOrNull() + runCatching { pattern?.let { Regex(it) } }.getOrNull() } } catch (e: Exception) { emptyList() @@ -136,7 +137,13 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo // entries against them by adding a trailing /. // TODO: Add unit tests for gitignore matching val relative = if (path.startsWith(projectRootPath.toString())) Paths.get(path).relativeTo(projectRootPath) else path - async { pattern.matches("$relative/") } + async { + try { + withTimeout(REGEX_TIMEOUT_MS) { pattern.matches("$relative/") } + } catch (e: Exception) { + false + } + } } } @@ -240,7 +247,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo tempFilePath } - private fun parseGitIgnore(): Set { + private fun parseGitIgnore(): Set { if (!gitIgnoreFile.exists()) { return emptySet() } @@ -252,46 +259,57 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo } // gitignore patterns are not regex, method update needed. - fun convertGitIgnorePatternToRegex(pattern: String): String = pattern - // Escape special regex characters except * and ? - .replace(".", "\\.") - .replace("+", "\\+") - .replace("(", "\\(") - .replace(")", "\\)") - .replace("[", "\\[") - .replace("]", "\\]") - .replace("{", "\\{") - .replace("}", "\\}") - .replace(",", "\\,") - .replace("^", "\\^") - .replace("$", "\\$") - .replace("|", "\\|") - // Convert gitignore glob patterns to regex - .replace("**", ".*?") // Match any directory depth - .replace("*", "[^/]*?") // Match any character except path separator - .replace("?", "[^/]") // Match single character except path separator - .let { pattern -> - when { - // If pattern starts with '/', anchor it to the start of the path - pattern.startsWith("/") -> "^${pattern.substring(1)}" - // If pattern doesn't start with '/', it can match anywhere in the path - else -> "(?:^|.*/?)$pattern" - } + fun convertGitIgnorePatternToRegex(pattern: String): String? { + // Skip invalid patterns for length check + if (pattern.length > MAX_PATTERN_LENGTH) { + return null } - .let { pattern -> - when { - // If pattern ends with '/', it should match directories - pattern.endsWith("/") -> "$pattern.*" - // Otherwise match exactly or with a trailing slash for directories - else -> "$pattern(?:/.*)?$" + return pattern + // Escape special regex characters except * and ? + .replace(".", "\\.") + .replace("+", "\\+") + .replace("(", "\\(") + .replace(")", "\\)") + .replace("[", "\\[") + .replace("]", "\\]") + .replace("{", "\\{") + .replace("}", "\\}") + .replace(",", "\\,") + .replace("^", "\\^") + .replace("$", "\\$") + .replace("|", "\\|") + // Convert gitignore glob patterns to regex + .replace("**", ".*?") // Match any directory depth + .replace("*", "[^/]*?") // Match any character except path separator + .replace("?", "[^/]") // Match single character except path separator + .let { pattern -> + when { + // If pattern starts with '/', anchor it to the start of the path + pattern.startsWith("/") -> "^${pattern.substring(1)}" + // If pattern doesn't start with '/', it can match anywhere in the path + else -> "(?:^|.*/?)$pattern" + } } - } + .let { pattern -> + when { + // If pattern ends with '/', it should match directories + pattern.endsWith("/") -> "$pattern.*" + // Otherwise match exactly or with a trailing slash for directories + else -> "$pattern(?:/.*)?$" + } + } + } var selectedSourceFolder: VirtualFile set(newRoot) { _selectedSourceFolder = newRoot } get() = _selectedSourceFolder + + companion object { + private const val MAX_PATTERN_LENGTH = 256 // Maximum allowed pattern length + private const val REGEX_TIMEOUT_MS = 100L // Timeout for regex operations in milliseconds + } } data class ZipCreationResult(val payload: File, val checksum: String, val contentLength: Long) From 37dc4414a886a763ee996929c40a5899c0d16aab Mon Sep 17 00:00:00 2001 From: Ashish Reddy Podduturi Date: Wed, 15 Jan 2025 13:54:31 -0800 Subject: [PATCH 5/8] detekt correction. --- .../jetbrains/services/amazonq/FeatureDevSessionContext.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt index 78d89442588..f172e438a0a 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt @@ -308,7 +308,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo companion object { private const val MAX_PATTERN_LENGTH = 256 // Maximum allowed pattern length - private const val REGEX_TIMEOUT_MS = 100L // Timeout for regex operations in milliseconds + private const val REGEX_TIMEOUT_MS = 100L // Timeout for regex operations in milliseconds } } From 4662ccaa6e14d266edb7f932eedd5e61f38e9960 Mon Sep 17 00:00:00 2001 From: Ashish Reddy Podduturi Date: Wed, 15 Jan 2025 15:33:16 -0800 Subject: [PATCH 6/8] detekt Main correction for Nameshadowing. --- .../amazonq/FeatureDevSessionContext.kt | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt index f172e438a0a..7c3e9f8fb42 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt @@ -264,8 +264,9 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo if (pattern.length > MAX_PATTERN_LENGTH) { return null } - return pattern - // Escape special regex characters except * and ? + + // Escape special regex characters except * and ? + var result = pattern .replace(".", "\\.") .replace("+", "\\+") .replace("(", "\\(") @@ -278,28 +279,29 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo .replace("^", "\\^") .replace("$", "\\$") .replace("|", "\\|") - // Convert gitignore glob patterns to regex + + // Convert gitignore glob patterns to regex + result = result .replace("**", ".*?") // Match any directory depth .replace("*", "[^/]*?") // Match any character except path separator .replace("?", "[^/]") // Match single character except path separator - .let { pattern -> - when { - // If pattern starts with '/', anchor it to the start of the path - pattern.startsWith("/") -> "^${pattern.substring(1)}" - // If pattern doesn't start with '/', it can match anywhere in the path - else -> "(?:^|.*/?)$pattern" - } - } - .let { pattern -> - when { - // If pattern ends with '/', it should match directories - pattern.endsWith("/") -> "$pattern.*" - // Otherwise match exactly or with a trailing slash for directories - else -> "$pattern(?:/.*)?$" - } - } - } + // Handle start of pattern + result = if (result.startsWith("/")) { + "^${result.substring(1)}" + } else { + "(?:^|.*/?)$result" + } + + // Handle end of pattern + result = if (result.endsWith("/")) { + "$result.*" + } else { + "$result(?:/.*)?$" + } + + return result + } var selectedSourceFolder: VirtualFile set(newRoot) { _selectedSourceFolder = newRoot From 89545c22b91cf8024aff09ba9772ca04d8cc78b8 Mon Sep 17 00:00:00 2001 From: Ashish Reddy Podduturi Date: Thu, 16 Jan 2025 12:59:39 -0800 Subject: [PATCH 7/8] Reverting the FeatureDevSessionContext pattern matching chnages. --- .../FeatureDevSessionContextTest.kt | 68 +------------------ .../amazonq/FeatureDevSessionContext.kt | 62 ++--------------- 2 files changed, 8 insertions(+), 122 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt index 0b4a84dfe47..136c4d3e433 100644 --- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt +++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt @@ -3,7 +3,6 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.testFramework.RuleChain -import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before @@ -118,72 +117,11 @@ class FeatureDevSessionContextTest : FeatureDevTestBase(HeavyJavaCodeInsightTest "gradlew.bat", "README.md", "gradle/wrapper/gradle-wrapper.properties", - "builder/GetTestBuilder.java" + "builder/GetTestBuilder.java", + "settings.gradle", + "build.gradle", ) assertTrue(zippedFiles == expectedFiles) } - - @Test - fun `test basic pattern conversion`() { - val input = "*.txt" - val expected = "(?:^|.*/?)[^/]*[^/]\\.txt(?:/.*)?\$" - assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) - } - - @Test - fun `test pattern with special characters`() { - val input = "test[abc].txt" - val expected = "(?:^|.*/?)test\\[abc\\]\\.txt(?:/.*)?$" - assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) - } - - @Test - fun `test pattern with double asterisk`() { - val input = "**/build" - val expected = "(?:^|.*/?).[^/]*[^/][^/]/build(?:/.*)?\$" - assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) - } - - @Test - fun `test pattern starting with slash`() { - val input = "/root/file.txt" - val expected = "^root/file\\.txt(?:/.*)?$" - assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) - } - - @Test - fun `test pattern ending with slash`() { - val input = "build/" - val expected = "(?:^|.*/?)build/.*" - assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) - } - - @Test - fun `test pattern with question mark`() { - val input = "file?.txt" - val expected = "(?:^|.*/?)file[^/]\\.txt(?:/.*)?$" - assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) - } - - @Test - fun `test complex pattern with multiple special characters`() { - val input = "**/test-[0-9]*.{java,kt}" - val expected = "(?:^|.*/?).[^/]*[^/][^/]/test-\\[0-9\\][^/]*[^/]\\.\\{java\\,kt\\}(?:/.*)?\$" - assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) - } - - @Test - fun `test empty pattern`() { - val input = "" - val expected = "(?:^|.*/?)(?:/.*)?$" - assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) - } - - @Test - fun `test pattern with all special regex characters`() { - val input = ".$+()[]{}^|" - val expected = "(?:^|.*/?)\\.\\\$\\+\\(\\)\\[\\]\\{\\}\\^\\|(?:/.*)?$" - assertEquals(expected, featureDevSessionContext.convertGitIgnorePatternToRegex(input)) - } } diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt index 7c3e9f8fb42..25a895d621a 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt @@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout import org.apache.commons.codec.digest.DigestUtils import org.apache.commons.io.FileUtils import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext @@ -137,13 +136,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo // entries against them by adding a trailing /. // TODO: Add unit tests for gitignore matching val relative = if (path.startsWith(projectRootPath.toString())) Paths.get(path).relativeTo(projectRootPath) else path - async { - try { - withTimeout(REGEX_TIMEOUT_MS) { pattern.matches("$relative/") } - } catch (e: Exception) { - false - } - } + async { pattern.matches("$relative/") } } } @@ -247,7 +240,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo tempFilePath } - private fun parseGitIgnore(): Set { + private fun parseGitIgnore(): Set { if (!gitIgnoreFile.exists()) { return emptySet() } @@ -259,59 +252,14 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo } // gitignore patterns are not regex, method update needed. - fun convertGitIgnorePatternToRegex(pattern: String): String? { - // Skip invalid patterns for length check - if (pattern.length > MAX_PATTERN_LENGTH) { - return null - } - - // Escape special regex characters except * and ? - var result = pattern - .replace(".", "\\.") - .replace("+", "\\+") - .replace("(", "\\(") - .replace(")", "\\)") - .replace("[", "\\[") - .replace("]", "\\]") - .replace("{", "\\{") - .replace("}", "\\}") - .replace(",", "\\,") - .replace("^", "\\^") - .replace("$", "\\$") - .replace("|", "\\|") - - // Convert gitignore glob patterns to regex - result = result - .replace("**", ".*?") // Match any directory depth - .replace("*", "[^/]*?") // Match any character except path separator - .replace("?", "[^/]") // Match single character except path separator - - // Handle start of pattern - result = if (result.startsWith("/")) { - "^${result.substring(1)}" - } else { - "(?:^|.*/?)$result" - } - - // Handle end of pattern - result = if (result.endsWith("/")) { - "$result.*" - } else { - "$result(?:/.*)?$" - } - - return result - } + fun convertGitIgnorePatternToRegex(pattern: String): String = pattern.replace(".", "\\.") + .replace("*", ".*") + .let { if (it.endsWith("/")) "$it.*" else "$it/.*" } // Add a trailing /* to all patterns. (we add a trailing / to all files when matching) var selectedSourceFolder: VirtualFile set(newRoot) { _selectedSourceFolder = newRoot } get() = _selectedSourceFolder - - companion object { - private const val MAX_PATTERN_LENGTH = 256 // Maximum allowed pattern length - private const val REGEX_TIMEOUT_MS = 100L // Timeout for regex operations in milliseconds - } } data class ZipCreationResult(val payload: File, val checksum: String, val contentLength: Long) From 1a8c60974db0785bc165a6a0d020f7efbd9e81d3 Mon Sep 17 00:00:00 2001 From: Ashish Reddy Podduturi Date: Thu, 16 Jan 2025 13:01:55 -0800 Subject: [PATCH 8/8] Correcting the chnages for FeatureDevSessionContext --- .../jetbrains/services/amazonq/FeatureDevSessionContext.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt index 25a895d621a..1005a1d9b4c 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt @@ -97,7 +97,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo addAll(additionalGitIgnoreRules.map { convertGitIgnorePatternToRegex(it) }) addAll(parseGitIgnore()) }.mapNotNull { pattern -> - runCatching { pattern?.let { Regex(it) } }.getOrNull() + runCatching { Regex(pattern) }.getOrNull() } } catch (e: Exception) { emptyList() @@ -252,7 +252,8 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo } // gitignore patterns are not regex, method update needed. - fun convertGitIgnorePatternToRegex(pattern: String): String = pattern.replace(".", "\\.") + private fun convertGitIgnorePatternToRegex(pattern: String): String = pattern + .replace(".", "\\.") .replace("*", ".*") .let { if (it.endsWith("/")) "$it.*" else "$it/.*" } // Add a trailing /* to all patterns. (we add a trailing / to all files when matching) var selectedSourceFolder: VirtualFile