Skip to content

Commit ac0fdf7

Browse files
Merge master into feature/cwltail
2 parents 1a635b1 + 1db0f97 commit ac0fdf7

File tree

4 files changed

+203
-39
lines changed

4 files changed

+203
-39
lines changed

packages/amazonq/test/unit/codewhisperer/util/codeParsingUtil.test.ts

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,17 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { extractClasses, extractFunctions, isTestFile, utgLanguageConfigs } from 'aws-core-vscode/codewhisperer'
6+
import * as vscode from 'vscode'
7+
import {
8+
PlatformLanguageId,
9+
extractClasses,
10+
extractFunctions,
11+
isTestFile,
12+
utgLanguageConfigs,
13+
} from 'aws-core-vscode/codewhisperer'
14+
import * as path from 'path'
715
import assert from 'assert'
16+
import { createTestWorkspaceFolder, toFile } from 'aws-core-vscode/test'
817

918
describe('RegexValidationForPython', () => {
1019
it('should extract all function names from a python file content', () => {
@@ -57,6 +66,99 @@ describe('RegexValidationForJava', () => {
5766
})
5867

5968
describe('isTestFile', () => {
69+
let testWsFolder: string
70+
beforeEach(async function () {
71+
testWsFolder = (await createTestWorkspaceFolder()).uri.fsPath
72+
})
73+
74+
it('validate by file path', async function () {
75+
const langs = new Map<string, string>([
76+
['java', '.java'],
77+
['python', '.py'],
78+
['typescript', '.ts'],
79+
['javascript', '.js'],
80+
['typescriptreact', '.tsx'],
81+
['javascriptreact', '.jsx'],
82+
])
83+
const testFilePathsWithoutExt = [
84+
'/test/MyClass',
85+
'/test/my_class',
86+
'/tst/MyClass',
87+
'/tst/my_class',
88+
'/tests/MyClass',
89+
'/tests/my_class',
90+
]
91+
92+
const srcFilePathsWithoutExt = [
93+
'/src/MyClass',
94+
'MyClass',
95+
'foo/bar/MyClass',
96+
'foo/my_class',
97+
'my_class',
98+
'anyFolderOtherThanTest/foo/myClass',
99+
]
100+
101+
for (const [languageId, ext] of langs) {
102+
const testFilePaths = testFilePathsWithoutExt.map((it) => it + ext)
103+
for (const testFilePath of testFilePaths) {
104+
const actual = await isTestFile(testFilePath, { languageId: languageId })
105+
assert.strictEqual(actual, true)
106+
}
107+
108+
const srcFilePaths = srcFilePathsWithoutExt.map((it) => it + ext)
109+
for (const srcFilePath of srcFilePaths) {
110+
const actual = await isTestFile(srcFilePath, { languageId: languageId })
111+
assert.strictEqual(actual, false)
112+
}
113+
}
114+
})
115+
116+
async function assertIsTestFile(
117+
fileNames: string[],
118+
config: { languageId: PlatformLanguageId },
119+
expected: boolean
120+
) {
121+
for (const fileName of fileNames) {
122+
const p = path.join(testWsFolder, fileName)
123+
await toFile('', p)
124+
const document = await vscode.workspace.openTextDocument(p)
125+
const actual = await isTestFile(document.uri.fsPath, { languageId: config.languageId })
126+
assert.strictEqual(actual, expected)
127+
}
128+
}
129+
130+
it('validate by file name', async function () {
131+
const camelCaseSrc = ['Foo.java', 'Bar.java', 'Baz.java']
132+
await assertIsTestFile(camelCaseSrc, { languageId: 'java' }, false)
133+
134+
const camelCaseTst = ['FooTest.java', 'BarTests.java']
135+
await assertIsTestFile(camelCaseTst, { languageId: 'java' }, true)
136+
137+
const snakeCaseSrc = ['foo.py', 'bar.py']
138+
await assertIsTestFile(snakeCaseSrc, { languageId: 'python' }, false)
139+
140+
const snakeCaseTst = ['test_foo.py', 'bar_test.py']
141+
await assertIsTestFile(snakeCaseTst, { languageId: 'python' }, true)
142+
143+
const javascriptSrc = ['Foo.js', 'bar.js']
144+
await assertIsTestFile(javascriptSrc, { languageId: 'javascript' }, false)
145+
146+
const javascriptTst = ['Foo.test.js', 'Bar.spec.js']
147+
await assertIsTestFile(javascriptTst, { languageId: 'javascript' }, true)
148+
149+
const typescriptSrc = ['Foo.ts', 'bar.ts']
150+
await assertIsTestFile(typescriptSrc, { languageId: 'typescript' }, false)
151+
152+
const typescriptTst = ['Foo.test.ts', 'Bar.spec.ts']
153+
await assertIsTestFile(typescriptTst, { languageId: 'typescript' }, true)
154+
155+
const jsxSrc = ['Foo.jsx', 'Bar.jsx']
156+
await assertIsTestFile(jsxSrc, { languageId: 'javascriptreact' }, false)
157+
158+
const jsxTst = ['Foo.test.jsx', 'Bar.spec.jsx']
159+
await assertIsTestFile(jsxTst, { languageId: 'javascriptreact' }, true)
160+
})
161+
60162
it('should return true if the file name matches the test filename pattern - Java', async () => {
61163
const filePaths = ['/path/to/MyClassTest.java', '/path/to/TestMyClass.java', '/path/to/MyClassTests.java']
62164
const language = 'java'

packages/amazonq/test/unit/codewhisperer/util/utgUtils.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,31 @@ describe('shouldFetchUtgContext', () => {
4444
assert.strictEqual(utgUtils.shouldFetchUtgContext('c', UserGroup.CrossFile), undefined)
4545
})
4646
})
47+
48+
describe('guessSrcFileName', function () {
49+
it('should return undefined if no matching regex', function () {
50+
assert.strictEqual(utgUtils.guessSrcFileName('Foo.java', 'java'), undefined)
51+
assert.strictEqual(utgUtils.guessSrcFileName('folder1/foo.py', 'python'), undefined)
52+
assert.strictEqual(utgUtils.guessSrcFileName('Bar.js', 'javascript'), undefined)
53+
})
54+
55+
it('java', function () {
56+
assert.strictEqual(utgUtils.guessSrcFileName('FooTest.java', 'java'), 'Foo.java')
57+
assert.strictEqual(utgUtils.guessSrcFileName('FooTests.java', 'java'), 'Foo.java')
58+
})
59+
60+
it('python', function () {
61+
assert.strictEqual(utgUtils.guessSrcFileName('test_foo.py', 'python'), 'foo.py')
62+
assert.strictEqual(utgUtils.guessSrcFileName('foo_test.py', 'python'), 'foo.py')
63+
})
64+
65+
it('typescript', function () {
66+
assert.strictEqual(utgUtils.guessSrcFileName('Foo.test.ts', 'typescript'), 'Foo.ts')
67+
assert.strictEqual(utgUtils.guessSrcFileName('Foo.spec.ts', 'typescript'), 'Foo.ts')
68+
})
69+
70+
it('javascript', function () {
71+
assert.strictEqual(utgUtils.guessSrcFileName('Foo.test.js', 'javascript'), 'Foo.js')
72+
assert.strictEqual(utgUtils.guessSrcFileName('Foo.spec.js', 'javascript'), 'Foo.js')
73+
})
74+
})

packages/core/src/codewhisperer/util/supplementalContext/codeParsingUtil.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,54 @@ import * as vscode from 'vscode'
77
import path = require('path')
88
import { normalize } from '../../../shared/utilities/pathUtils'
99

10+
// TODO: functionExtractionPattern, classExtractionPattern, imposrtStatementRegex are not scalable and we will deprecate and remove the usage in the near future
1011
export interface utgLanguageConfig {
1112
extension: string
12-
testFilenamePattern: RegExp
13-
functionExtractionPattern: RegExp
14-
classExtractionPattern: RegExp
15-
importStatementRegExp: RegExp
13+
testFilenamePattern: RegExp[]
14+
functionExtractionPattern?: RegExp
15+
classExtractionPattern?: RegExp
16+
importStatementRegExp?: RegExp
1617
}
1718

1819
export const utgLanguageConfigs: Record<string, utgLanguageConfig> = {
1920
// Java regexes are not working efficiently for class or function extraction
2021
java: {
2122
extension: '.java',
22-
testFilenamePattern: /(?:Test([^/\\]+)\.java|([^/\\]+)Test\.java|([^/\\]+)Tests\.java)$/,
23+
testFilenamePattern: [/^(.+)Test(\.java)$/, /(.+)Tests(\.java)$/, /Test(.+)(\.java)$/],
2324
functionExtractionPattern:
2425
/(?:(?:public|private|protected)\s+)(?:static\s+)?(?:[\w<>]+\s+)?(\w+)\s*\([^)]*\)\s*(?:(?:throws\s+\w+)?\s*)[{;]/gm, // TODO: Doesn't work for generice <T> T functions.
2526
classExtractionPattern: /(?<=^|\n)\s*public\s+class\s+(\w+)/gm, // TODO: Verify these.
2627
importStatementRegExp: /import .*\.([a-zA-Z0-9]+);/,
2728
},
2829
python: {
2930
extension: '.py',
30-
testFilenamePattern: /(?:test_([^/\\]+)\.py|([^/\\]+)_test\.py)$/,
31+
testFilenamePattern: [/^test_(.+)(\.py)$/, /^(.+)_test(\.py)$/],
3132
functionExtractionPattern: /def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g, // Worked fine
3233
classExtractionPattern: /^class\s+(\w+)\s*:/gm,
3334
importStatementRegExp: /from (.*) import.*/,
3435
},
36+
typescript: {
37+
extension: '.ts',
38+
testFilenamePattern: [/^(.+)\.test(\.ts|\.tsx)$/, /^(.+)\.spec(\.ts|\.tsx)$/],
39+
},
40+
javascript: {
41+
extension: '.js',
42+
testFilenamePattern: [/^(.+)\.test(\.js|\.jsx)$/, /^(.+)\.spec(\.js|\.jsx)$/],
43+
},
44+
typescriptreact: {
45+
extension: '.tsx',
46+
testFilenamePattern: [/^(.+)\.test(\.ts|\.tsx)$/, /^(.+)\.spec(\.ts|\.tsx)$/],
47+
},
48+
javascriptreact: {
49+
extension: '.jsx',
50+
testFilenamePattern: [/^(.+)\.test(\.js|\.jsx)$/, /^(.+)\.spec(\.js|\.jsx)$/],
51+
},
3552
}
3653

37-
export function extractFunctions(fileContent: string, regex: RegExp) {
54+
export function extractFunctions(fileContent: string, regex?: RegExp) {
55+
if (!regex) {
56+
return []
57+
}
3858
const functionNames: string[] = []
3959
let match: RegExpExecArray | null
4060

@@ -44,7 +64,10 @@ export function extractFunctions(fileContent: string, regex: RegExp) {
4464
return functionNames
4565
}
4666

47-
export function extractClasses(fileContent: string, regex: RegExp) {
67+
export function extractClasses(fileContent: string, regex?: RegExp) {
68+
if (!regex) {
69+
return []
70+
}
4871
const classNames: string[] = []
4972
let match: RegExpExecArray | null
5073

@@ -97,6 +120,11 @@ function isTestFileByName(filePath: string, language: vscode.TextDocument['langu
97120
const testFilenamePattern = languageConfig.testFilenamePattern
98121

99122
const filename = path.basename(filePath)
123+
for (const pattern of testFilenamePattern) {
124+
if (pattern.test(filename)) {
125+
return true
126+
}
127+
}
100128

101-
return testFilenamePattern.test(filename)
129+
return false
102130
}

packages/core/src/codewhisperer/util/supplementalContext/utgUtils.ts

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ import { getOpenFilesInWindow } from '../../../shared/utilities/editorUtilities'
2424
import { getLogger } from '../../../shared/logger/logger'
2525
import { CodeWhispererSupplementalContext, CodeWhispererSupplementalContextItem, UtgStrategy } from '../../models/model'
2626

27-
type UtgSupportedLanguage = keyof typeof utgLanguageConfigs
27+
const utgSupportedLanguages: vscode.TextDocument['languageId'][] = ['java', 'python']
28+
29+
type UtgSupportedLanguage = (typeof utgSupportedLanguages)[number]
2830

2931
function isUtgSupportedLanguage(languageId: vscode.TextDocument['languageId']): languageId is UtgSupportedLanguage {
30-
return languageId in utgLanguageConfigs
32+
return utgSupportedLanguages.includes(languageId)
3133
}
3234

3335
export function shouldFetchUtgContext(
@@ -184,41 +186,45 @@ async function getRelevantUtgFiles(editor: vscode.TextEditor): Promise<string[]>
184186
})
185187
}
186188

189+
export function guessSrcFileName(
190+
testFileName: string,
191+
languageId: vscode.TextDocument['languageId']
192+
): string | undefined {
193+
const languageConfig = utgLanguageConfigs[languageId]
194+
if (!languageConfig) {
195+
return undefined
196+
}
197+
198+
for (const pattern of languageConfig.testFilenamePattern) {
199+
try {
200+
const match = testFileName.match(pattern)
201+
if (match) {
202+
return match[1] + match[2]
203+
}
204+
} catch (err) {
205+
if (err instanceof Error) {
206+
getLogger().error(
207+
`codewhisperer: error while guessing source file name from file ${testFileName} and pattern ${pattern}: ${err.message}`
208+
)
209+
}
210+
}
211+
}
212+
213+
return undefined
214+
}
215+
187216
async function findSourceFileByName(
188217
editor: vscode.TextEditor,
189218
languageConfig: utgLanguageConfig,
190219
cancellationToken: vscode.CancellationToken
191220
): Promise<string | undefined> {
192221
const testFileName = path.basename(editor.document.fileName)
193-
194-
let basenameSuffix = testFileName
195-
const match = testFileName.match(languageConfig.testFilenamePattern)
196-
if (match) {
197-
basenameSuffix = match[1] || match[2]
198-
}
199-
200-
throwIfCancelled(cancellationToken)
201-
202-
// Assuming the convention of using similar path structure for test and src files.
203-
const dirPath = path.dirname(editor.document.uri.fsPath)
204-
let newPath = ''
205-
const lastIndexTest = dirPath.lastIndexOf('/test/')
206-
const lastIndexTst = dirPath.lastIndexOf('/tst/')
207-
// This is a faster way on the assumption that source file and test file will follow similar path structure.
208-
if (lastIndexTest > 0) {
209-
newPath = dirPath.substring(0, lastIndexTest) + '/src/' + dirPath.substring(lastIndexTest + 5)
210-
} else if (lastIndexTst > 0) {
211-
newPath = dirPath.substring(0, lastIndexTst) + '/src/' + dirPath.substring(lastIndexTst + 4)
212-
}
213-
newPath = path.join(newPath, basenameSuffix + languageConfig.extension)
214-
// TODO: Add metrics here, as we are not able to find the source file by name.
215-
if (await fs.exists(newPath)) {
216-
return newPath
222+
const assumedSrcFileName = guessSrcFileName(testFileName, editor.document.languageId)
223+
if (!assumedSrcFileName) {
224+
return undefined
217225
}
218226

219-
throwIfCancelled(cancellationToken)
220-
221-
const sourceFiles = await vscode.workspace.findFiles(`**/${basenameSuffix}${languageConfig.extension}`)
227+
const sourceFiles = await vscode.workspace.findFiles(`**/${assumedSrcFileName}`)
222228

223229
throwIfCancelled(cancellationToken)
224230

0 commit comments

Comments
 (0)