diff --git a/packages/vipeye-internal/src/tests/chainedSourceFiles.test.ts b/packages/vipeye-internal/src/tests/chainedSourceFiles.test.ts deleted file mode 100644 index fd9c2bfa..00000000 --- a/packages/vipeye-internal/src/tests/chainedSourceFiles.test.ts +++ /dev/null @@ -1,491 +0,0 @@ -/* - * chainedSourceFiles.test.ts - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * - * Unit tests for tokenizer ipython mode - */ - -import assert from 'assert'; -import { CancellationToken } from 'vscode-jsonrpc'; -import { MarkupKind } from 'vscode-languageserver-types'; - -import { Program } from '../analyzer/program'; -import { AnalyzerService } from '../analyzer/service'; -import { IPythonMode } from '../analyzer/sourceFile'; -import { ConfigOptions } from '../common/configOptions'; -import { NullConsole } from '../common/console'; -import { normalizeSlashes } from '../common/pathUtils'; -import { convertOffsetsToRange, convertOffsetToPosition } from '../common/positionUtils'; -import { ServiceProvider } from '../common/serviceProvider'; -import { Uri } from '../common/uri/uri'; -import { UriEx } from '../common/uri/uriUtils'; -import { CompletionProvider } from '../languageService/completionProvider'; -import { parseTestData } from './harness/fourslash/fourSlashParser'; -import { TestAccessHost } from './harness/testAccessHost'; -import * as host from './harness/testHost'; -import { createFromFileSystem, distlibFolder, libFolder } from './harness/vfs/factory'; -import * as vfs from './harness/vfs/filesystem'; - -test('check chained files', () => { - const code = ` -// @filename: test1.py -//// def foo1(): pass - -// @filename: test2.py -//// def foo2(): pass - -// @filename: test3.py -//// def foo3(): pass - -// @filename: test4.py -//// [|foo/*marker*/|] - `; - - const basePath = UriEx.file(normalizeSlashes('/')); - const { data, service } = createServiceWithChainedSourceFiles(basePath, code); - - const marker = data.markerPositions.get('marker')!; - const markerUri = marker.fileUri; - - const parseResult = service.getParseResults(markerUri)!; - const result = new CompletionProvider( - service.test_program, - markerUri, - convertOffsetToPosition(marker.position, parseResult.tokenizerOutput.lines), - { - format: MarkupKind.Markdown, - lazyEdit: false, - snippet: false, - }, - CancellationToken.None - ).getCompletions(); - - assert(result?.items.some((i) => i.label === 'foo1')); - assert(result?.items.some((i) => i.label === 'foo2')); - assert(result?.items.some((i) => i.label === 'foo3')); -}); - -test('modify chained files', () => { - const code = ` -// @filename: test1.py -//// def foo1(): pass - -// @filename: test2.py -//// [|/*delete*/|] -//// def foo2(): pass - -// @filename: test3.py -//// def foo3(): pass - -// @filename: test4.py -//// [|foo/*marker*/|] - `; - - const basePath = UriEx.file(normalizeSlashes('/')); - const { data, service } = createServiceWithChainedSourceFiles(basePath, code); - - // Make sure files are all realized. - const marker = data.markerPositions.get('marker')!; - const markerUri = marker.fileUri; - const parseResult = service.getParseResults(markerUri)!; - - // Close file in the middle of the chain - service.setFileClosed(data.markerPositions.get('delete')!.fileUri); - - // Make sure we don't get suggestion from auto import but from chained files. - service.test_program.configOptions.autoImportCompletions = false; - - const result = new CompletionProvider( - service.test_program, - markerUri, - convertOffsetToPosition(marker.position, parseResult.tokenizerOutput.lines), - { - format: MarkupKind.Markdown, - lazyEdit: false, - snippet: false, - }, - CancellationToken.None - ).getCompletions(); - - assert(result); - - assert(!result.items.some((i) => i.label === 'foo1')); - assert(!result.items.some((i) => i.label === 'foo2')); - assert(result.items.some((i) => i.label === 'foo3')); -}); - -test('modify chained files', async () => { - const code = ` -// @filename: test1.py -//// [|/*changed*/|] -//// def foo1(): pass - -// @filename: test2.py -//// def foo2(): pass - -// @filename: test3.py -//// def foo3(): pass - -// @filename: test4.py -//// [|/*marker*/foo1()|] - `; - - const basePath = UriEx.file(normalizeSlashes('/')); - const { data, service } = createServiceWithChainedSourceFiles(basePath, code); - - const marker = data.markerPositions.get('marker')!; - const markerUri = marker.fileUri; - const range = data.ranges.find((r) => r.marker === marker)!; - - const parseResults = service.getParseResults(markerUri)!; - analyze(service.test_program); - - // Initially, there should be no error. - const initialDiags = await service.getDiagnosticsForRange( - markerUri, - convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines), - CancellationToken.None - ); - - assert.strictEqual(initialDiags.length, 0); - - // Change test1 content - service.updateOpenFileContents(data.markerPositions.get('changed')!.fileUri, 2, 'def foo5(): pass'); - analyze(service.test_program); - - const finalDiags = await service.getDiagnosticsForRange( - markerUri, - convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines), - CancellationToken.None - ); - - assert.strictEqual(finalDiags.length, 1); -}); - -function generateChainedFiles(count: number, lastFile: string) { - let code = ''; - for (let i = 0; i < count; i++) { - code += ` -// @filename: test${i + 1}.py -//// def foo${i + 1}(): pass -`; - } - code += lastFile; - return code; -} - -test('chained files with 1000s of files', async () => { - const lastFile = ` -// @filename: testFinal.py -//// [|/*marker*/foo1()|] - `; - const code = generateChainedFiles(1000, lastFile); - const basePath = UriEx.file(normalizeSlashes('/')); - const { data, service } = createServiceWithChainedSourceFiles(basePath, code); - const marker = data.markerPositions.get('marker')!; - const markerUri = marker.fileUri; - const range = data.ranges.find((r) => r.marker === marker)!; - - const parseResults = service.getParseResults(markerUri)!; - analyze(service.test_program); - - // There should be no error as it should find the foo1 in the first chained file. - const initialDiags = await service.getDiagnosticsForRange( - markerUri, - convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines), - CancellationToken.None - ); - - assert.strictEqual(initialDiags.length, 0); -}); - -test('imported by files', async () => { - const code = ` -// @filename: test1.py -//// import [|/*marker*/os|] - -// @filename: test2.py -//// os.path.join() - `; - - const basePath = UriEx.file(normalizeSlashes('/')); - const { data, service } = createServiceWithChainedSourceFiles(basePath, code); - analyze(service.test_program); - - const marker = data.markerPositions.get('marker')!; - const markerUri = marker.fileUri; - const range = data.ranges.find((r) => r.marker === marker)!; - - const parseResults = service.getParseResults(markerUri)!; - const diagnostics = await service.getDiagnosticsForRange( - markerUri, - convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines), - CancellationToken.None - ); - - assert.strictEqual(diagnostics.length, 0); -}); - -test('re ordering cells', async () => { - const code = ` -// @filename: test1.py -//// import [|/*marker*/os|] - -// @filename: test2.py -//// /*bottom*/os.path.join() - `; - - const basePath = UriEx.file(normalizeSlashes('/')); - const { data, service } = createServiceWithChainedSourceFiles(basePath, code); - analyze(service.test_program); - - const marker = data.markerPositions.get('marker')!; - const markerUri = marker.fileUri; - const range = data.ranges.find((r) => r.marker === marker)!; - - const bottom = data.markerPositions.get('bottom')!; - const bottomUri = bottom.fileUri; - - service.updateChainedUri(bottomUri, undefined); - service.updateChainedUri(markerUri, bottomUri); - analyze(service.test_program); - - const parseResults = service.getParseResults(markerUri)!; - const diagnostics = await service.getDiagnosticsForRange( - markerUri, - convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines), - CancellationToken.None - ); - - assert.strictEqual(diagnostics.length, 1); -}); - -test('later chained-file globals resolved in function body', async () => { - const code = ` -// @filename: test1.py -//// def use_x(): -//// [|/*marker*/return x + 1|] - -// @filename: test2.py -//// x = 42 - `; - - const basePath = UriEx.file(normalizeSlashes('/')); - const { data, service } = createServiceWithChainedSourceFiles(basePath, code); - analyze(service.test_program); - - const marker = data.markerPositions.get('marker')!; - const markerUri = marker.fileUri; - const range = data.ranges.find((r) => r.marker === marker)!; - - const parseResults = service.getParseResults(markerUri)!; - const diagnostics = await service.getDiagnosticsForRange( - markerUri, - convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines), - CancellationToken.None - ); - - assert.strictEqual(diagnostics.length, 0); -}); - -test('later chained-file globals resolved in lambda body', async () => { - const code = ` -// @filename: test1.py -//// [|/*marker*/f = lambda: x * 2|] - -// @filename: test2.py -//// x = 10 - `; - - const basePath = UriEx.file(normalizeSlashes('/')); - const { data, service } = createServiceWithChainedSourceFiles(basePath, code); - analyze(service.test_program); - - const marker = data.markerPositions.get('marker')!; - const markerUri = marker.fileUri; - const range = data.ranges.find((r) => r.marker === marker)!; - - const parseResults = service.getParseResults(markerUri)!; - const diagnostics = await service.getDiagnosticsForRange( - markerUri, - convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines), - CancellationToken.None - ); - - assert.strictEqual(diagnostics.length, 0); -}); - -test('later chained-file globals resolved in class header', async () => { - const code = ` -// @filename: test1.py -//// [|/*marker*/class MyClass(Base):|] -//// pass - -// @filename: test2.py -//// class Base: -//// pass - `; - - const basePath = UriEx.file(normalizeSlashes('/')); - const { data, service } = createServiceWithChainedSourceFiles(basePath, code); - analyze(service.test_program); - - const marker = data.markerPositions.get('marker')!; - const markerUri = marker.fileUri; - const range = data.ranges.find((r) => r.marker === marker)!; - - const parseResults = service.getParseResults(markerUri)!; - const diagnostics = await service.getDiagnosticsForRange( - markerUri, - convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines), - CancellationToken.None - ); - - assert.strictEqual(diagnostics.length, 0); -}); - -test('later chained-file globals resolved in comprehension inside function', async () => { - const code = ` -// @filename: test1.py -//// def build_list(): -//// [|/*marker*/return [i * x for i in [1, 2, 3]]|] - -// @filename: test2.py -//// x = 5 - `; - - const basePath = UriEx.file(normalizeSlashes('/')); - const { data, service } = createServiceWithChainedSourceFiles(basePath, code); - analyze(service.test_program); - - const marker = data.markerPositions.get('marker')!; - const markerUri = marker.fileUri; - const range = data.ranges.find((r) => r.marker === marker)!; - - const parseResults = service.getParseResults(markerUri)!; - const diagnostics = await service.getDiagnosticsForRange( - markerUri, - convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines), - CancellationToken.None - ); - - assert.strictEqual(diagnostics.length, 0); -}); - -test('later chained-file globals resolved in nested function', async () => { - const code = ` -// @filename: test1.py -//// def outer(): -//// def inner(): -//// [|/*marker*/return x|] -//// return inner - -// @filename: test2.py -//// x = 99 - `; - - const basePath = UriEx.file(normalizeSlashes('/')); - const { data, service } = createServiceWithChainedSourceFiles(basePath, code); - analyze(service.test_program); - - const marker = data.markerPositions.get('marker')!; - const markerUri = marker.fileUri; - const range = data.ranges.find((r) => r.marker === marker)!; - - const parseResults = service.getParseResults(markerUri)!; - const diagnostics = await service.getDiagnosticsForRange( - markerUri, - convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines), - CancellationToken.None - ); - - assert.strictEqual(diagnostics.length, 0); -}); - -test('later chained-file globals not visible without chaining', async () => { - // Verify that the same code produces errors without chained files - // (i.e. our test scenarios actually catch a real bug). - const code = ` -// @filename: test1.py -//// def use_x(): -//// [|/*marker*/return x + 1|] - `; - - const basePath = UriEx.file(normalizeSlashes('/')); - const { data, service } = createServiceWithChainedSourceFiles(basePath, code); - analyze(service.test_program); - - const marker = data.markerPositions.get('marker')!; - const markerUri = marker.fileUri; - const range = data.ranges.find((r) => r.marker === marker)!; - - const parseResults = service.getParseResults(markerUri)!; - const diagnostics = await service.getDiagnosticsForRange( - markerUri, - convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines), - CancellationToken.None - ); - - // Should have an error because x is not defined anywhere - assert(diagnostics.length > 0); -}); - -test('later chained-file globals with multiple files and mixed scopes', async () => { - const code = ` -// @filename: test1.py -//// def use_both(): -//// [|/*marker*/return x + y|] - -// @filename: test2.py -//// x = 42 - -// @filename: test3.py -//// y = 100 - `; - - const basePath = UriEx.file(normalizeSlashes('/')); - const { data, service } = createServiceWithChainedSourceFiles(basePath, code); - analyze(service.test_program); - - const marker = data.markerPositions.get('marker')!; - const markerUri = marker.fileUri; - const range = data.ranges.find((r) => r.marker === marker)!; - - const parseResults = service.getParseResults(markerUri)!; - const diagnostics = await service.getDiagnosticsForRange( - markerUri, - convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines), - CancellationToken.None - ); - - assert.strictEqual(diagnostics.length, 0); -}); - -function createServiceWithChainedSourceFiles(basePath: Uri, code: string) { - const fs = createFromFileSystem(host.HOST, /*ignoreCase*/ false, { cwd: basePath.getFilePath() }); - const service = new AnalyzerService('test service', new ServiceProvider(), { - console: new NullConsole(), - hostFactory: () => new TestAccessHost(UriEx.file(vfs.MODULE_PATH), [libFolder, distlibFolder]), - importResolverFactory: AnalyzerService.createImportResolver, - configOptions: new ConfigOptions(basePath), - fileSystem: fs, - shouldRunAnalysis: () => true, - }); - - const data = parseTestData(basePath.getFilePath(), code, ''); - - let chainedFilePath: Uri | undefined; - for (const file of data.files) { - const uri = file.fileUri; - service.setFileOpened(uri, 1, file.content, IPythonMode.CellDocs, chainedFilePath); - chainedFilePath = uri; - } - return { data, service }; -} - -function analyze(program: Program) { - while (program.analyze()) { - // Process all queued items - } -} diff --git a/packages/vipeye-internal/src/tests/classDeclaration.test.ts b/packages/vipeye-internal/src/tests/classDeclaration.test.ts deleted file mode 100644 index 9b2d39a4..00000000 --- a/packages/vipeye-internal/src/tests/classDeclaration.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -/* - * classDeclaration.test.ts - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * - * Test class detail's declaration - */ - -import assert from 'assert'; - -import { isClassDeclaration, isSpecialBuiltInClassDeclaration } from '../analyzer/declaration'; -import { getEnclosingFunction } from '../analyzer/parseTreeUtils'; -import { isProperty } from '../analyzer/typeUtils'; -import { TypeCategory, isClassInstance } from '../analyzer/types'; -import { TextRange } from '../common/textRange'; -import { ParseNodeType } from '../parser/parseNodes'; -import { Range } from './harness/fourslash/fourSlashTypes'; -import { TestState, getNodeAtMarker, parseAndGetTestState } from './harness/fourslash/testState'; - -test('regular class', () => { - const code = ` -// @filename: test.py -//// [|class /*marker*/A: -//// pass|] - `; - - checkClassDetail(code); -}); - -test('Meta class', () => { - const code = ` -// @filename: test.py -//// [|class /*range*/MyMeta(type): -//// def __new__(cls, name, bases, dct): -//// return super().__new__(cls, name, bases, dct)|] -//// -//// class MyClass(metaclass=MyMeta): -//// pass -//// -//// /*marker*/E = MyMeta() - `; - - checkClassDetail(code, '__class_MyMeta'); -}); - -test('special built in class', () => { - const code = ` -// @filename: test.py -//// from typing import TypedDict -//// def foo(t: /*marker*/TypedDict): ... - `; - - checkSpecialBuiltInClassDetail(code); -}); - -test('dynamic enum', () => { - const code = ` -// @filename: test.py -//// from enum import Enum -//// /*marker*/E = Enum('E', { 'One': 1 }) - `; - - checkNoDeclarationInClassDetail(code); -}); - -test('dynamic named tuple', () => { - const code = ` -// @filename: test.py -//// from typing import NamedTuple -//// /*marker*/N = NamedTuple("N", [('name', str)]) - `; - - checkNoDeclarationInClassDetail(code); -}); - -test('dynamic typed dict', () => { - const code = ` -// @filename: test.py -//// from typing import TypedDict -//// /*marker*/T = TypedDict("T", { "one": str }) - `; - - checkNoDeclarationInClassDetail(code); -}); - -test('dynamic new type', () => { - const code = ` -// @filename: test.py -//// from typing import NewType -//// /*marker*/I = NewType('I', int) - `; - - checkNoDeclarationInClassDetail(code); -}); - -test('dynamic type', () => { - const code = ` -// @filename: test.py -//// /*marker*/D = type('D', (object,), {}) - `; - - checkNoDeclarationInClassDetail(code); -}); - -test('property', () => { - const code = ` -// @filename: test.py -//// class MyClass: -//// def __init__(self): -//// self._v = None -//// -//// @property -//// def /*getter*/value(self): -//// return self._v -//// -//// @value.setter -//// def /*setter*/value(self, value): -//// self._v = value -//// -//// @value.deleter -//// def /*deleter*/value(self): -//// del self._v - `; - - const state = parseAndGetTestState(code).state; - - ['getter', 'setter', 'deleter'].forEach((marker) => { - const node = getNodeAtMarker(state, marker); - assert(node.nodeType === ParseNodeType.Name); - - const functionNode = getEnclosingFunction(node); - assert(functionNode?.nodeType === ParseNodeType.Function); - - const result = state.program.evaluator!.getTypeOfFunction(functionNode); - assert(result?.decoratedType); - - assert(isProperty(result.decoratedType)); - assert(isClassInstance(result.decoratedType)); - - assert(result.decoratedType.shared.declaration); - assert(isClassDeclaration(result.decoratedType.shared.declaration)); - - assert(result.decoratedType.shared.declaration.moduleName === 'builtins'); - assert(result.decoratedType.shared.declaration.node.d.name.d.value === 'property'); - }); -}); - -function checkSpecialBuiltInClassDetail(code: string) { - const state = parseAndGetTestState(code).state; - - const node = getNodeAtMarker(state); - assert(node.nodeType === ParseNodeType.Name); - - const type = state.program.evaluator!.getType(node); - assert(type?.category === TypeCategory.Class); - - assert.strictEqual(node.d.value, type.priv.aliasName ?? type.shared.name); - - assert(type.shared.declaration); - if (type.priv.aliasName) { - assert(isClassDeclaration(type.shared.declaration)); - } else { - assert(isSpecialBuiltInClassDeclaration(type.shared.declaration)); - } -} - -function checkNoDeclarationInClassDetail(code: string) { - const state = parseAndGetTestState(code).state; - _checkClassDetail(state, undefined); -} - -function checkClassDetail(code: string, name?: string) { - const state = parseAndGetTestState(code).state; - _checkClassDetail(state, state.getRangeByMarkerName('marker') ?? state.getRangeByMarkerName('range'), name); -} - -function _checkClassDetail(state: TestState, range: Range | undefined, name?: string) { - const node = getNodeAtMarker(state); - assert(node.nodeType === ParseNodeType.Name); - - const type = state.program.evaluator!.getType(node); - assert(type?.category === TypeCategory.Class); - - assert.strictEqual(name ?? node.d.value, type.priv.aliasName ?? type.shared.name); - - if (range) { - assert(type.shared.declaration); - assert(isClassDeclaration(type.shared.declaration)); - - assert.deepStrictEqual( - TextRange.create(type.shared.declaration.node.start, type.shared.declaration.node.length), - TextRange.fromBounds(range.pos, range.end) - ); - } else { - // There should be no decl. - assert(!type.shared.declaration); - } -} diff --git a/packages/vipeye-internal/src/tests/completions.test.ts b/packages/vipeye-internal/src/tests/completions.test.ts deleted file mode 100644 index 33be6cb9..00000000 --- a/packages/vipeye-internal/src/tests/completions.test.ts +++ /dev/null @@ -1,2178 +0,0 @@ -/* - * completions.test.ts - * - * completions tests. - */ - -import assert from 'assert'; -import { CancellationToken } from 'vscode-languageserver'; -import { ApplyKind, CompletionItemKind, MarkupKind } from 'vscode-languageserver-types'; - -import { Uri } from '../common/uri/uri'; -import { CompletionItemData, CompletionOptions, CompletionProvider } from '../languageService/completionProvider'; -import { parseAndGetTestState } from './harness/fourslash/testState'; - -test('completion import statement tooltip', async () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import [|/*marker*/m|] - -// @filename: matplotlib/__init__.py -// @library: true -//// """ matplotlib """ - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Module, - label: 'matplotlib', - documentation: 'matplotlib', - }, - ], - }, - }); -}); - -test('completion import statement tooltip - stub file', async () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import [|/*marker*/m|] - -// @filename: matplotlib/__init__.pyi -// @library: true -//// # empty - -// @filename: matplotlib/__init__.py -// @library: true -//// """ matplotlib """ - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Module, - label: 'matplotlib', - documentation: 'matplotlib', - }, - ], - }, - }); -}); - -test('completion import statement tooltip - doc in stub file', async () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import [|/*marker*/m|] - -// @filename: matplotlib/__init__.pyi -// @library: true -//// """ matplotlib """ - -// @filename: matplotlib/__init__.py -// @library: true -//// # empty - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Module, - label: 'matplotlib', - documentation: 'matplotlib', - }, - ], - }, - }); -}); - -test('completion import statement tooltip - sub modules', async () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import matplotlib.[|/*marker*/p|] - -// @filename: matplotlib/__init__.py -// @library: true -//// """ matplotlib """ - -// @filename: matplotlib/pyplot.py -// @library: true -//// """ pyplot """ - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Module, - label: 'pyplot', - documentation: 'pyplot', - }, - ], - }, - }); -}); - -test('completion import reference tooltip', async () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import matplotlib -//// [|/*marker*/m|] - -// @filename: matplotlib/__init__.py -// @library: true -//// """ matplotlib """ - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Module, - label: 'matplotlib', - documentation: '```python\nmatplotlib\n```\n---\nmatplotlib', - }, - ], - }, - }); -}); - -test('completion import reference tooltip - first module', async () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import matplotlib.pyplot -//// [|/*marker*/m|] - -// @filename: matplotlib/__init__.py -// @library: true -//// """ matplotlib """ - -// @filename: matplotlib/pyplot.py -// @library: true -//// """ pyplot """ - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Module, - label: 'matplotlib', - documentation: '```python\nmatplotlib\n```\n---\nmatplotlib', - }, - ], - }, - }); -}); - -test('completion import reference tooltip - child module', async () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import matplotlib.pyplot -//// matplotlib.[|/*marker*/p|] - -// @filename: matplotlib/__init__.py -// @library: true -//// """ matplotlib """ - -// @filename: matplotlib/pyplot.py -// @library: true -//// """ pyplot """ - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Module, - label: 'pyplot', - documentation: '```python\npyplot\n```\n---\npyplot', - }, - ], - }, - }); -}); - -test('completion from import statement tooltip - first module', async () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// from [|/*marker*/m|] - -// @filename: matplotlib/__init__.py -// @library: true -//// """ matplotlib """ - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Module, - label: 'matplotlib', - documentation: 'matplotlib', - }, - ], - }, - }); -}); - -test('completion from import statement tooltip - child module', async () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// from matplotlib.[|/*marker*/p|] - -// @filename: matplotlib/__init__.py -// @library: true -//// """ matplotlib """ - -// @filename: matplotlib/pyplot.py -// @library: true -//// """ pyplot """ - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Module, - label: 'pyplot', - documentation: 'pyplot', - }, - ], - }, - }); -}); - -test('completion from import statement tooltip - implicit module', async () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// from matplotlib import [|/*marker*/p|] - -// @filename: matplotlib/__init__.py -// @library: true -//// """ matplotlib """ - -// @filename: matplotlib/pyplot.py -// @library: true -//// """ pyplot """ - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Module, - label: 'pyplot', - documentation: 'pyplot', - }, - ], - }, - }); -}); - -test('include literals in expression completion', async () => { - const code = ` -// @filename: test.py -//// from typing import TypedDict -//// -//// class TestType(TypedDict): -//// A: str -//// B: int -//// -//// var: TestType = {} -//// -//// var[[|A/*marker*/|]] - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: "'A'", - textEdit: { range: state.getPositionRange('marker'), newText: "'A'" }, - }, - ], - }, - }); -}); - -test('include literals in set key', async () => { - const code = ` -// @filename: test.py -//// from typing import TypedDict -//// -//// class TestType(TypedDict): -//// A: str -//// B: int -//// -//// var: TestType = { [|A/*marker*/|] } - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: "'A'", - textEdit: { range: state.getPositionRange('marker'), newText: "'A'" }, - }, - ], - }, - }); -}); - -test('include literals in dict key', async () => { - const code = ` -// @filename: test.py -//// from typing import TypedDict -//// -//// class TestType(TypedDict): -//// A: str -//// B: int -//// -//// var: TestType = { [|A/*marker*/|] : "hello" } - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"A"', - textEdit: { range: state.getPositionRange('marker'), newText: '"A"' }, - }, - ], - }, - }); -}); - -test('literals support for binary operators - equals', async () => { - const code = ` -// @filename: test.py -//// from typing import Literal -//// -//// Currency = Literal["USD", "EUR"] -//// -//// def foo(c: Currency): -//// if c == [|"/*marker*/"|] - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"USD"', - textEdit: { range: state.getPositionRange('marker'), newText: '"USD"' }, - }, - { - kind: CompletionItemKind.Constant, - label: '"EUR"', - textEdit: { range: state.getPositionRange('marker'), newText: '"EUR"' }, - }, - ], - }, - }); -}); - -test('literals support for binary operators - not equals', async () => { - const code = ` -// @filename: test.py -//// from typing import Literal -//// -//// Currency = Literal["USD", "EUR"] -//// -//// def foo(c: Currency): -//// if c != [|"/*marker*/"|] - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"USD"', - textEdit: { range: state.getPositionRange('marker'), newText: '"USD"' }, - }, - { - kind: CompletionItemKind.Constant, - label: '"EUR"', - textEdit: { range: state.getPositionRange('marker'), newText: '"EUR"' }, - }, - ], - }, - }); -}); - -test('literals support for binary operators without string node', async () => { - const code = ` -// @filename: test.py -//// from typing import Literal -//// -//// Currency = Literal["USD", "EUR"] -//// -//// def foo(c: Currency): -//// if c != [|/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"USD"', - }, - { - kind: CompletionItemKind.Constant, - label: '"EUR"', - }, - ], - }, - }); -}); - -test('literals support for binary operators with prior word', async () => { - const code = ` -// @filename: test.py -//// from typing import Literal -//// -//// Currency = Literal["USD", "EUR"] -//// -//// def foo(c: Currency): -//// if c != [|US/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"USD"', - }, - ], - }, - }); -}); - -test('literals support for binary operators - assignment expression', async () => { - const code = ` -// @filename: test.py -//// from typing import Literal -//// -//// Currency = Literal["USD", "EUR"] -//// -//// def foo(c: Currency): -//// if c := [|"/*marker*/"|] - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"USD"', - textEdit: { range: state.getPositionRange('marker'), newText: '"USD"' }, - }, - { - kind: CompletionItemKind.Constant, - label: '"EUR"', - textEdit: { range: state.getPositionRange('marker'), newText: '"EUR"' }, - }, - ], - }, - }); -}); - -test('literals support for call', async () => { - const code = ` -// @filename: test.py -//// from typing import Literal -//// -//// Currency = Literal["USD", "EUR"] -//// -//// def foo(c: Currency) -> Currency: -//// return c -//// -//// if foo([|"/*marker1*/"|]) == [|"/*marker2*/"|] - `; - - const state = parseAndGetTestState(code).state; - const marker1 = state.getMarkerByName('marker1'); - state.openFile(marker1.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker1: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"USD"', - textEdit: { range: state.getPositionRange('marker1'), newText: '"USD"' }, - }, - { - kind: CompletionItemKind.Constant, - label: '"EUR"', - textEdit: { range: state.getPositionRange('marker1'), newText: '"EUR"' }, - }, - ], - }, - marker2: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"USD"', - textEdit: { range: state.getPositionRange('marker2'), newText: '"USD"' }, - }, - { - kind: CompletionItemKind.Constant, - label: '"EUR"', - textEdit: { range: state.getPositionRange('marker2'), newText: '"EUR"' }, - }, - ], - }, - }); -}); - -test('list with literal types', async () => { - const code = ` -// @filename: test.py -//// from typing import Literal -//// -//// Currency = Literal["USD", "EUR"] -//// -//// a: list[Currency] = [[|"/*marker*/"|]] - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"USD"', - textEdit: { range: state.getPositionRange('marker'), newText: '"USD"' }, - }, - { - kind: CompletionItemKind.Constant, - label: '"EUR"', - textEdit: { range: state.getPositionRange('marker'), newText: '"EUR"' }, - }, - ], - }, - }); -}); - -test('literals support for match - error case', async () => { - const code = ` -// @filename: test.py -//// from typing import Literal -//// -//// Currency = Literal["USD", "EUR"] -//// -//// def foo(c: Currency): -//// match c: -//// case [|/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"USD"', - }, - { - kind: CompletionItemKind.Constant, - label: '"EUR"', - }, - ], - }, - }); -}); - -test('literals support for match - simple case', async () => { - const code = ` -// @filename: test.py -//// from typing import Literal -//// -//// Currency = Literal["USD", "EUR"] -//// -//// def foo(c: Currency): -//// match c: -//// case [|"/*marker*/"|] - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"USD"', - textEdit: { range: state.getPositionRange('marker'), newText: '"USD"' }, - }, - { - kind: CompletionItemKind.Constant, - label: '"EUR"', - textEdit: { range: state.getPositionRange('marker'), newText: '"EUR"' }, - }, - ], - }, - }); -}); - -test('literals support for match - simple case without string', async () => { - const code = ` -// @filename: test.py -//// from typing import Literal -//// -//// Currency = Literal["USD", "EUR"] -//// -//// def foo(c: Currency): -//// match c: -//// case [|US/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"USD"', - }, - ], - }, - }); -}); - -test('completion quote trigger', async () => { - const code = ` -// @filename: test.py -//// from typing import Literal -//// -//// Currency = Literal["USD", "EUR"] -//// -//// def foo(c: Currency): -//// match c: -//// case [|"/*marker*/"|] - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - const filePath = marker.fileName; - const uri = Uri.file(filePath, state.serviceProvider); - const position = state.convertOffsetToPosition(filePath, marker.position); - - const options: CompletionOptions = { - format: 'markdown', - snippet: false, - lazyEdit: false, - triggerCharacter: '"', - }; - - const result = new CompletionProvider( - state.program, - uri, - position, - options, - CancellationToken.None - ).getCompletions(); - - assert(result); - const item = result.items.find((a) => a.label === '"USD"'); - assert(item); -}); - -test('completion quote trigger - middle', async () => { - const code = ` -// @filename: test.py -//// from typing import Literal -//// -//// Currency = Literal["Quote'Middle"] -//// -//// def foo(c: Currency): -//// match c: -//// case [|"Quote'/*marker*/"|] - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - const filePath = marker.fileName; - const uri = Uri.file(filePath, state.serviceProvider); - const position = state.convertOffsetToPosition(filePath, marker.position); - - const options: CompletionOptions = { - format: 'markdown', - snippet: false, - lazyEdit: false, - triggerCharacter: "'", - }; - - const result = new CompletionProvider( - state.program, - uri, - position, - options, - CancellationToken.None - ).getCompletions(); - - assert.strictEqual(result?.items.length, 0); -}); - -test('auto import sort text', async () => { - const code = ` -// @filename: test.py -//// [|os/*marker*/|] - -// @filename: unused.py -//// import os -//// p = os.path - -// @filename: vendored/__init__.py -// @library: true -//// # empty - -// @filename: vendored/os.py -// @library: true -//// def foo(): pass - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFiles(state.testData.files.map((f) => f.fileName)); - - while (state.workspace.service.test_program.analyze()); - - const filePath = marker.fileName; - const uri = Uri.file(filePath, state.serviceProvider); - const position = state.convertOffsetToPosition(filePath, marker.position); - - const options: CompletionOptions = { - format: 'markdown', - snippet: false, - lazyEdit: false, - }; - - const result = new CompletionProvider( - state.program, - uri, - position, - options, - CancellationToken.None - ).getCompletions(); - - const items = result?.items.filter((i) => i.label === 'os'); - assert.strictEqual(items?.length, 2); - - items.sort((a, b) => a.sortText!.localeCompare(b.sortText!)); - - assert(!items[0].labelDetails); - assert.strictEqual(items[1].labelDetails!.description, 'vendored'); -}); - -test('completion MRU affects sort order', async () => { - type RecentCompletionInfo = { - label: string; - autoImportText: string; - }; - - const completionProviderTestAccess = CompletionProvider as unknown as { - [key: string]: RecentCompletionInfo[]; - }; - - // Reset MRU list to keep the test deterministic. - completionProviderTestAccess._mostRecentCompletions = []; - - const code = ` -// @filename: test.py -//// true_divide = 0 -//// truly = 0 -//// tru/*marker*/ - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFiles(state.testData.files.map((f) => f.fileName)); - - while (state.workspace.service.test_program.analyze()); - - const filePath = marker.fileName; - const uri = Uri.file(filePath, state.serviceProvider); - const position = state.convertOffsetToPosition(filePath, marker.position); - - const options: CompletionOptions = { - format: 'markdown', - snippet: false, - lazyEdit: false, - }; - - const provider1 = new CompletionProvider(state.program, uri, position, options, CancellationToken.None); - const result1 = provider1.getCompletions(); - assert(result1); - - const truly1 = result1.items.find((i) => i.label === 'truly'); - const trueDivide1 = result1.items.find((i) => i.label === 'true_divide'); - assert(truly1?.sortText); - assert(trueDivide1?.sortText); - - // Not in MRU list yet: both share the same category, so neither is promoted. - assert.strictEqual(truly1.sortText.split('.')[0], trueDivide1.sortText.split('.')[0]); - - // Accepting 'truly' records it in the MRU. (MRU is updated on accept, not on passive resolve.) - CompletionProvider.recordCompletionAccepted('truly', ''); - - const provider2 = new CompletionProvider(state.program, uri, position, options, CancellationToken.None); - const result2 = provider2.getCompletions(); - assert(result2); - - const truly2 = result2.items.find((i) => i.label === 'truly'); - const trueDivide2 = result2.items.find((i) => i.label === 'true_divide'); - assert(truly2?.sortText); - assert(trueDivide2?.sortText); - - // Now the selected item is in MRU and is promoted to an earlier category. Compare relative - // ordering instead of hard-coding enum-derived SortCategory prefixes (which shift whenever a new - // SortCategory is inserted). - assert(truly2.sortText < truly1.sortText); // promoted ahead of its own pre-MRU position - assert(truly2.sortText < trueDivide2.sortText); // and ahead of the non-MRU sibling - assert.strictEqual(trueDivide2.sortText.split('.')[0], trueDivide1.sortText.split('.')[0]); // sibling unchanged - - // Reset MRU list so it doesn't affect other tests. - completionProviderTestAccess._mostRecentCompletions = []; -}); - -test('passive resolve does not change MRU sort order', async () => { - type RecentCompletionInfo = { - label: string; - autoImportText: string; - }; - - const completionProviderTestAccess = CompletionProvider as unknown as { - [key: string]: RecentCompletionInfo[]; - }; - - // Reset MRU list to keep the test deterministic. - completionProviderTestAccess._mostRecentCompletions = []; - - const code = ` -// @filename: test.py -//// true_divide = 0 -//// truly = 0 -//// tru/*marker*/ - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFiles(state.testData.files.map((f) => f.fileName)); - - while (state.workspace.service.test_program.analyze()); - - const filePath = marker.fileName; - const uri = Uri.file(filePath, state.serviceProvider); - const position = state.convertOffsetToPosition(filePath, marker.position); - - const options: CompletionOptions = { - format: 'markdown', - snippet: false, - lazyEdit: false, - }; - - const provider1 = new CompletionProvider(state.program, uri, position, options, CancellationToken.None); - const result1 = provider1.getCompletions(); - assert(result1); - - const truly1 = result1.items.find((i) => i.label === 'truly'); - const trueDivide1 = result1.items.find((i) => i.label === 'true_divide'); - assert(truly1?.sortText); - assert(trueDivide1?.sortText); - assert.strictEqual(truly1.sortText.split('.')[0], trueDivide1.sortText.split('.')[0]); - - // Passive resolve (fired per keystroke while previewing inline suggestions) must NOT mutate the - // MRU; otherwise the ranking oscillates as the user types. - await provider1.resolveCompletionItem(truly1); - - const provider2 = new CompletionProvider(state.program, uri, position, options, CancellationToken.None); - const result2 = provider2.getCompletions(); - assert(result2); - - const truly2 = result2.items.find((i) => i.label === 'truly'); - const trueDivide2 = result2.items.find((i) => i.label === 'true_divide'); - assert(truly2?.sortText); - assert(trueDivide2?.sortText); - - // Sort order is unchanged because resolve did not record anything in the MRU. - assert.strictEqual(truly2.sortText.split('.')[0], trueDivide2.sortText.split('.')[0]); - assert.strictEqual(completionProviderTestAccess._mostRecentCompletions.length, 0); - - // Reset MRU list so it doesn't affect other tests. - completionProviderTestAccess._mostRecentCompletions = []; -}); - -test('override generic', async () => { - const code = ` -// @filename: test.py -//// from typing import Generic, TypeVar -//// from typing_extensions import override -//// -//// T = TypeVar('T') -//// class A(Generic[T]): -//// def foo(self, x: list[T]) -> T: -//// return x -//// -//// class B(A[int]): -//// @override -//// def [|foo/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('included', 'markdown', { - ['marker']: { - completions: [ - { - label: 'foo', - kind: CompletionItemKind.Method, - textEdit: { - range: state.getPositionRange('marker'), - newText: 'foo(self, x: list[T]) -> T:\n return super().foo(x)', - }, - }, - ], - }, - }); -}); - -test('override generic nested', async () => { - const code = ` -// @filename: test.py -//// from typing import Generic, TypeVar -//// from typing_extensions import override -//// -//// T = TypeVar('T') -//// T2 = TypeVar('T2') -//// class A(Generic[T, T2]): -//// def foo(self, x: tuple[T, T2]) -> T: -//// return x -//// -//// -//// T3 = TypeVar('T3') -//// class B(A[int, T3]): -//// @override -//// def [|foo/*marker1*/|] -//// -//// class C(B[int]): -//// @override -//// def [|foo/*marker2*/|] - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('included', 'markdown', { - ['marker1']: { - completions: [ - { - label: 'foo', - kind: CompletionItemKind.Method, - textEdit: { - range: state.getPositionRange('marker1'), - newText: 'foo(self, x: tuple[T, T2]) -> T:\n return super().foo(x)', - }, - }, - ], - }, - ['marker2']: { - completions: [ - { - label: 'foo', - kind: CompletionItemKind.Method, - textEdit: { - range: state.getPositionRange('marker2'), - newText: 'foo(self, x: tuple[T, T2]) -> T:\n return super().foo(x)', - }, - }, - ], - }, - }); -}); - -test('override __call__', async () => { - const code = ` -// @filename: test.py -//// from argparse import Action -//// -//// class MyAction(Action): -//// def [|__call__/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('included', 'markdown', { - ['marker']: { - completions: [ - { - label: '__call__', - kind: CompletionItemKind.Method, - textEdit: { - range: state.getPositionRange('marker'), - newText: - '__call__(self, parser: ArgumentParser, namespace: Namespace, values: str | Sequence[Any] | None, option_string: str | None = None) -> None:\n return super().__call__(parser, namespace, values, option_string)', - }, - }, - ], - }, - }); -}); - -test('override ParamSpec', async () => { - const code = ` -// @filename: test.py -//// from typing import Callable, ParamSpec -//// -//// P = ParamSpec("P") -//// -//// class A: -//// def foo(self, func: Callable[P, None], *args: P.args, **kwargs: P.kwargs): -//// pass -//// -//// class B(A): -//// def [|foo/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('included', 'markdown', { - ['marker']: { - completions: [ - { - label: 'foo', - kind: CompletionItemKind.Method, - textEdit: { - range: state.getPositionRange('marker'), - newText: - 'foo(self, func: Callable[P, None], *args: P.args, **kwargs: P.kwargs):\n return super().foo(func, *args, **kwargs)', - }, - }, - ], - }, - }); -}); - -test('annotation using comment', async () => { - const code = ` -// @filename: test.py -//// class A: -//// def foo(self, a): # type: (int) -> None -//// pass -//// -//// class B(A): -//// def [|foo/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('included', 'markdown', { - ['marker']: { - completions: [ - { - label: 'foo', - kind: CompletionItemKind.Method, - textEdit: { - range: state.getPositionRange('marker'), - newText: 'foo(self, a: int) -> None:\n return super().foo(a)', - }, - }, - ], - }, - }); -}); - -test('Complex type arguments', async () => { - const code = ` -// @filename: test.py -//// from typing import Generic, TypeVar, Any, List, Dict, Tuple, Mapping, Union -//// -//// T = TypeVar("T") -//// -//// class A(Generic[T]): -//// def foo(self, a: T) -> T: -//// return a -//// -//// class B(A[Union[Tuple[list, dict], tuple[Mapping[List[A[int]], Dict[str, Any]], float]]]): -//// pass - -// @filename: test1.py -//// from test import B -//// -//// class U(B): -//// def [|foo/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - - state.openFiles(state.testData.files.map((f) => f.fileName)); - - await state.verifyCompletion('included', 'markdown', { - marker: { - completions: [ - { - label: 'foo', - kind: CompletionItemKind.Method, - textEdit: { - range: state.getPositionRange('marker'), - newText: 'foo(self, a: T) -> T:\n return super().foo(a)', - }, - }, - ], - }, - }); -}); - -test('Enum member', async () => { - const code = ` -// @filename: test.py -//// from enum import Enum -//// -//// class MyEnum(Enum): -//// this = 1 -//// that = 2 -//// -//// print(MyEnum.[|/*marker*/|]) - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('included', 'markdown', { - ['marker']: { - completions: [ - { - label: 'this', - kind: CompletionItemKind.EnumMember, - documentation: '```python\nthis: int\n```', - }, - ], - }, - }); -}); - -test('no member of Enum member', async () => { - const code = ` -// @filename: test.py -//// from enum import Enum -//// -//// class MyEnum(Enum): -//// this = 1 -//// that = 2 -//// -//// print(MyEnum.this.[|/*marker*/|]) - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('excluded', 'markdown', { - ['marker']: { - completions: [ - { - label: 'this', - kind: undefined, - }, - { - label: 'that', - kind: undefined, - }, - ], - }, - }); -}); - -test('default Enum member', async () => { - const code = ` -// @filename: test.py -//// from enum import Enum -//// -//// class MyEnum(Enum): -//// MemberOne = [] -//// -//// MyEnum.MemberOne.[|/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('included', 'markdown', { - ['marker']: { - completions: [ - { - label: 'name', - kind: CompletionItemKind.Property, - }, - { - label: 'value', - kind: CompletionItemKind.Property, - }, - ], - }, - }); -}); - -test('str-backed Enum comparison suggests member values', async () => { - const code = ` -// @filename: test.py -//// from enum import Enum -//// -//// class Mode(str, Enum): -//// Train = "train" -//// Test = "test" -//// -//// config: Mode = Mode("train") -//// -//// if config == [|"/*marker*/"|]: -//// pass -`; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', 'markdown', { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"train"', - textEdit: { range: state.getPositionRange('marker'), newText: '"train"' }, - }, - { - kind: CompletionItemKind.Constant, - label: '"test"', - textEdit: { range: state.getPositionRange('marker'), newText: '"test"' }, - }, - ], - }, - }); -}); - -test('TypeDict literal values', async () => { - const code = ` -// @filename: test.py -//// from typing import TypedDict, Literal -//// -//// class DataA(TypedDict): -//// name: Literal["a", "b"] | None -//// -//// data_a: DataA = { -//// "name": [|"/*marker*/"|] -//// } - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('included', 'markdown', { - ['marker']: { - completions: [ - { - label: '"a"', - kind: CompletionItemKind.Constant, - textEdit: { range: state.getPositionRange('marker'), newText: '"a"' }, - }, - { - label: '"b"', - kind: CompletionItemKind.Constant, - textEdit: { range: state.getPositionRange('marker'), newText: '"b"' }, - }, - ], - }, - }); -}); - -test('typed dict key constructor completion', async () => { - const code = ` -// @filename: test.py -//// from typing import TypedDict -//// -//// class Movie(TypedDict): -//// key1: str -//// -//// a = Movie(k[|"/*marker*/"|]) -//// - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('included', MarkupKind.Markdown, { - marker: { - completions: [ - { - kind: CompletionItemKind.Variable, - label: 'key1=', - }, - ], - }, - }); -}); - -test('import from completion for namespace package', async () => { - const code = ` -// @filename: test.py -//// from nest1 import [|/*marker*/|] - -// @filename: nest1/nest2/__init__.py -//// # empty - -// @filename: nest1/module.py -//// # empty - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('included', 'markdown', { - ['marker']: { - completions: [ - { - label: 'nest2', - kind: CompletionItemKind.Module, - }, - { - label: 'module', - kind: CompletionItemKind.Module, - }, - ], - }, - }); -}); - -test('members off enum member', async () => { - const code = ` -// @filename: test.py -//// from enum import Enum -//// class Planet(Enum): -//// MERCURY = (3.303e+23, 2.4397e6) -//// EARTH = (5.976e+24, 6.37814e6) -//// -//// def __init__(self, mass, radius): -//// self.mass = mass # in kilograms -//// self.radius = radius # in meters -//// -//// @property -//// def surface_gravity(self): -//// # universal gravitational constant (m3 kg-1 s-2) -//// G = 6.67300E-11 -//// return G * self.mass / (self.radius * self.radius) -//// -//// Planet.EARTH.[|/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('excluded', 'markdown', { - ['marker']: { - completions: [ - { - label: 'MERCURY', - kind: CompletionItemKind.EnumMember, - }, - { - label: 'EARTH', - kind: CompletionItemKind.EnumMember, - }, - ], - }, - }); - - await state.verifyCompletion('included', 'markdown', { - ['marker']: { - completions: [ - { - label: 'mass', - kind: CompletionItemKind.Variable, - }, - { - label: 'radius', - kind: CompletionItemKind.Variable, - }, - { - label: 'surface_gravity', - kind: CompletionItemKind.Property, - }, - ], - }, - }); -}); - -test('handle missing close paren case', async () => { - const code = ` -// @filename: test.py -//// count=100 -//// while count <= (c[|/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('included', 'markdown', { - ['marker']: { - completions: [ - { - label: 'count', - kind: CompletionItemKind.Variable, - }, - ], - }, - }); -}); - -test('enum with regular base type', async () => { - const code = ` -// @filename: test.py -//// from enum import Enum -//// from datetime import timedelta -//// class Period(timedelta, Enum): -//// Today = -1 -//// -//// Period.Today.[|/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('included', 'markdown', { - ['marker']: { - completions: [ - { - label: 'days', - kind: CompletionItemKind.Property, - }, - { - label: 'seconds', - kind: CompletionItemKind.Property, - }, - ], - }, - }); -}); - -test('import statements with implicit import', async () => { - const code = ` -// @filename: test.py -//// from lib import /*marker*/ - -// @filename: lib/__init__.py -//// from . import api as api - -// @filename: lib/api.py -//// # Empty - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('included', 'markdown', { - ['marker']: { - completions: [ - { - label: 'api', - kind: CompletionItemKind.Module, - }, - ], - }, - }); -}); - -test('overloaded Literal[...] suggestions in call arguments', async () => { - const code = ` -// @filename: test.py -//// from typing import overload, Literal -//// -//// @overload -//// def example(p: Literal["A"]): ... -//// @overload -//// def example(p: Literal["B"]): ... -//// @overload -//// def example(p: Literal["C"]): ... -//// def example(p): -//// pass -//// -//// example([|"/*marker*/"|]) - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', 'markdown', { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"A"', - textEdit: { range: state.getPositionRange('marker'), newText: '"A"' }, - }, - { - kind: CompletionItemKind.Constant, - label: '"B"', - textEdit: { range: state.getPositionRange('marker'), newText: '"B"' }, - }, - { - kind: CompletionItemKind.Constant, - label: '"C"', - textEdit: { range: state.getPositionRange('marker'), newText: '"C"' }, - }, - ], - }, - }); -}); - -test('collection literal suggestions in call arguments', async () => { - const code = ` -// @filename: test.py -//// from typing import Any, Collection, Literal, overload -//// -//// @overload -//// def dumps(obj: Any, *, allow: Collection[Literal["nan"]] = ()) -> str: ... -//// @overload -//// def dumps(obj: Any, *, allow: Collection[str] = ()) -> str: ... -//// -//// def dumps(obj: Any, *, allow: Collection[str] = ()) -> str: -//// return '' -//// -//// dumps(None, allow=[[|"/*marker*/"|]]) - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', 'markdown', { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"nan"', - textEdit: { range: state.getPositionRange('marker'), newText: '"nan"' }, - }, - ], - }, - }); -}); - -test('collection literal suggestions include all literal overload element candidates', async () => { - const code = ` -// @filename: test.py -//// from typing import Any, Collection, Literal, overload -//// -//// @overload -//// def dumps(obj: Any, *, allow: Collection[Literal["nan"]] = ()) -> str: ... -//// @overload -//// def dumps(obj: Any, *, allow: Collection[Literal["inf"]] = ()) -> str: ... -//// -//// def dumps(obj: Any, *, allow: Collection[str] = ()) -> str: -//// return '' -//// -//// dumps(None, allow=[[|"/*marker*/"|]]) - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', 'markdown', { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"nan"', - textEdit: { range: state.getPositionRange('marker'), newText: '"nan"' }, - }, - { - kind: CompletionItemKind.Constant, - label: '"inf"', - textEdit: { range: state.getPositionRange('marker'), newText: '"inf"' }, - }, - ], - }, - }); -}); - -test('collection literal suggestions exclude deeply nested containers in call arguments', async () => { - const code = ` -// @filename: test.py -//// from typing import Any, Collection, Literal, overload -//// -//// @overload -//// def dumps(obj: Any, *, allow: Collection[Literal["nan"]] = ()) -> str: ... -//// @overload -//// def dumps(obj: Any, *, allow: Collection[str] = ()) -> str: ... -//// -//// def dumps(obj: Any, *, allow: Collection[str] = ()) -> str: -//// return '' -//// -//// dumps(None, allow=[[[[|"/*marker*/"|]]]]) - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('excluded', 'markdown', { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"nan"', - textEdit: { range: state.getPositionRange('marker'), newText: '"nan"' }, - }, - ], - }, - }); -}); - -test('typing list literal suggestions in call arguments', async () => { - const code = ` -// @filename: test.py -//// from typing import Any, List, Literal, overload -//// -//// @overload -//// def dumps(obj: Any, *, allow: List[Literal["nan"]]) -> str: ... -//// @overload -//// def dumps(obj: Any, *, allow: List[str]) -> str: ... -//// -//// def dumps(obj: Any, *, allow: List[str]) -> str: -//// return '' -//// -//// dumps(None, allow=[[|"/*marker*/"|]]) - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', 'markdown', { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"nan"', - textEdit: { range: state.getPositionRange('marker'), newText: '"nan"' }, - }, - ], - }, - }); -}); - -test('mapping value literal suggestions include all overload candidates', async () => { - const code = ` -// @filename: test.py -//// from typing import Any, Literal, Mapping, overload -//// -//// @overload -//// def encode(obj: Any, *, mapping: Mapping[str, Literal["nan"]] = ...) -> str: ... -//// @overload -//// def encode(obj: Any, *, mapping: Mapping[str, Literal["inf"]] = ...) -> str: ... -//// -//// def encode(obj: Any, *, mapping: Mapping[str, str] = ...) -> str: -//// return '' -//// -//// encode(None, mapping={"key": [|"/*marker*/"|]}) - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', 'markdown', { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"nan"', - textEdit: { range: state.getPositionRange('marker'), newText: '"nan"' }, - }, - { - kind: CompletionItemKind.Constant, - label: '"inf"', - textEdit: { range: state.getPositionRange('marker'), newText: '"inf"' }, - }, - ], - }, - }); -}); - -test('sequence literal suggestions in call arguments', async () => { - const code = ` -// @filename: test.py -//// from typing import Any, Literal, Sequence, overload -//// -//// @overload -//// def encode(obj: Any, *, seq: Sequence[Literal["nan"]] = ...) -> str: ... -//// @overload -//// def encode(obj: Any, *, seq: Sequence[str] = ...) -> str: ... -//// -//// def encode(obj: Any, *, seq: Sequence[str] = ...) -> str: -//// return '' -//// -//// encode(None, seq=[[|"/*marker*/"|]]) - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', 'markdown', { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"nan"', - textEdit: { range: state.getPositionRange('marker'), newText: '"nan"' }, - }, - ], - }, - }); -}); - -test('collection literal suggestions include comprehensions in call arguments', async () => { - const code = ` -// @filename: test.py -//// from typing import Any, Collection, Literal, overload -//// -//// @overload -//// def dumps(obj: Any, *, allow: Collection[Literal["nan"]] = ()) -> str: ... -//// @overload -//// def dumps(obj: Any, *, allow: Collection[str] = ()) -> str: ... -//// -//// def dumps(obj: Any, *, allow: Collection[str] = ()) -> str: -//// return '' -//// -//// dumps(None, allow=[[|"/*marker*/"|] for _ in range(1)]) - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', 'markdown', { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"nan"', - textEdit: { range: state.getPositionRange('marker'), newText: '"nan"' }, - }, - ], - }, - }); -}); - -test('collection literal suggestions exclude lambdas in call arguments', async () => { - const code = ` -// @filename: test.py -//// from typing import Any, Collection, Literal, overload -//// -//// @overload -//// def dumps(obj: Any, *, allow: Collection[Literal["nan"]] = ()) -> str: ... -//// @overload -//// def dumps(obj: Any, *, allow: Collection[str] = ()) -> str: ... -//// -//// def dumps(obj: Any, *, allow: Collection[str] = ()) -> str: -//// return '' -//// -//// dumps(None, allow=[lambda: [|"/*marker*/"|]]) - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('excluded', 'markdown', { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"nan"', - textEdit: { range: state.getPositionRange('marker'), newText: '"nan"' }, - }, - ], - }, - }); -}); - -test('nested TypedDict completion with Unpack - without other fields', async () => { - const code = ` -// @filename: test.py -//// from typing import Unpack, TypedDict -//// -//// class InnerDict(TypedDict): -//// a: int -//// b: str -//// -//// class OuterDict(TypedDict): -//// inner: InnerDict -//// field_1: str -//// -//// def test_inner_dict(**kwargs: Unpack[OuterDict]): -//// pass -//// -//// test_inner_dict(inner={[|/*marker*/|]}) - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('included', 'markdown', { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: "'a'", - textEdit: { range: state.getPositionRange('marker'), newText: "'a'" }, - }, - { - kind: CompletionItemKind.Constant, - label: "'b'", - textEdit: { range: state.getPositionRange('marker'), newText: "'b'" }, - }, - ], - }, - }); -}); - -test('nested TypedDict completion with Unpack - with other fields', async () => { - const code = ` -// @filename: test.py -//// from typing import Unpack, TypedDict -//// -//// class InnerDict(TypedDict): -//// a: int -//// b: str -//// -//// class OuterDict(TypedDict): -//// inner: InnerDict -//// field_1: str -//// -//// def test_inner_dict(**kwargs: Unpack[OuterDict]): -//// pass -//// -//// test_inner_dict(field_1="test", inner={[|/*marker*/|]}) - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('included', 'markdown', { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"a"', - textEdit: { range: state.getPositionRange('marker'), newText: '"a"' }, - }, - { - kind: CompletionItemKind.Constant, - label: '"b"', - textEdit: { range: state.getPositionRange('marker'), newText: '"b"' }, - }, - ], - }, - }); -}); - -test('simple nested TypedDict completion - no Unpack', async () => { - const code = ` -// @filename: test.py -//// from typing import TypedDict -//// -//// class InnerDict(TypedDict): -//// a: int -//// b: str -//// -//// def test_func(inner: InnerDict): -//// pass -//// -//// test_func(inner={[|/*marker*/|]}) - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('included', 'markdown', { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: "'a'", - textEdit: { range: state.getPositionRange('marker'), newText: "'a'" }, - }, - { - kind: CompletionItemKind.Constant, - label: "'b'", - textEdit: { range: state.getPositionRange('marker'), newText: "'b'" }, - }, - ], - }, - }); -}); - -test('TypedDict subscript completion with Literal assignment target', async () => { - const code = ` -// @filename: test.py -//// from typing import Literal, TypedDict, TypeAlias -//// -//// SomeLiterals: TypeAlias = Literal["literal1", "literal2"] -//// -//// class Settings(TypedDict): -//// value: SomeLiterals -//// -//// class Test: -//// def __init__(self) -> None: -//// self.settings: Settings = {"value": "literal1"} -//// -//// def meth(self, literal: SomeLiterals): -//// literal = self.settings[[|/*marker*/|]] - `; - - const state = parseAndGetTestState(code).state; - - await state.verifyCompletion('included', 'markdown', { - marker: { - completions: [ - { - kind: CompletionItemKind.Constant, - label: '"value"', - textEdit: { range: state.getPositionRange('marker'), newText: '"value"' }, - }, - ], - }, - }); - - await state.verifyCompletion('excluded', 'markdown', { - marker: { - completions: [ - { - label: '"literal1"', - kind: CompletionItemKind.Constant, - }, - { - label: '"literal2"', - kind: CompletionItemKind.Constant, - }, - ], - }, - }); -}); - -test('completion itemDefaults.data hoist when capability enabled', async () => { - const code = ` -// @filename: test.py -//// def my_function(): ... -//// my_/*marker*/ - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - const filePath = marker.fileName; - const uri = Uri.file(filePath, state.serviceProvider); - const position = state.convertOffsetToPosition(filePath, marker.position); - - const options: CompletionOptions = { - format: 'markdown', - snippet: false, - lazyEdit: false, - completionItemDataDefault: true, - }; - - const result = new CompletionProvider( - state.program, - uri, - position, - options, - CancellationToken.None - ).getCompletions(); - - assert(result); - assert(result.items.length > 0); - - // The shared `uri`/`position` are hoisted into `itemDefaults.data`, and the client is told to - // merge it back per item via `applyKind.data === Merge`. - assert.deepStrictEqual(result.itemDefaults?.data, { uri: uri.toString(), position }); - assert.strictEqual(result.applyKind?.data, ApplyKind.Merge); - - const item = result.items.find((i) => i.label === 'my_function'); - assert(item); - - // Per-item data no longer carries the duplicated `uri`/`position`, but retains item-specific fields. - const itemData = item.data as Partial; - assert.strictEqual(itemData.uri, undefined); - assert.strictEqual(itemData.position, undefined); - assert.strictEqual(itemData.symbolLabel, 'my_function'); -}); - -test('completion itemDefaults.data not hoisted when capability disabled', async () => { - const code = ` -// @filename: test.py -//// def my_function(): ... -//// my_/*marker*/ - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - const filePath = marker.fileName; - const uri = Uri.file(filePath, state.serviceProvider); - const position = state.convertOffsetToPosition(filePath, marker.position); - - const options: CompletionOptions = { - format: 'markdown', - snippet: false, - lazyEdit: false, - }; - - const result = new CompletionProvider( - state.program, - uri, - position, - options, - CancellationToken.None - ).getCompletions(); - - assert(result); - assert.strictEqual(result.itemDefaults?.data, undefined); - assert.strictEqual(result.applyKind, undefined); - - const item = result.items.find((i) => i.label === 'my_function'); - assert(item); - - // Without the capability, each item still carries the full `uri`/`position` payload. - const itemData = item.data as Partial; - assert.strictEqual(itemData.uri, uri.toString()); - assert.deepStrictEqual(itemData.position, position); -}); diff --git a/packages/vipeye-internal/src/tests/diagnostics.test.ts b/packages/vipeye-internal/src/tests/diagnostics.test.ts deleted file mode 100644 index 8c88a369..00000000 --- a/packages/vipeye-internal/src/tests/diagnostics.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * diagnostics.test.ts - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * - * Unit tests for diagnostics - */ - -import assert from 'assert'; - -import { DiagnosticRule } from '../common/diagnosticRules'; -import { Range } from '../common/textRange'; -import { parseAndGetTestState } from './harness/fourslash/testState'; - -// A range that spans the whole document so range intersection never filters diagnostics out. -const wholeFileRange: Range = { - start: { line: 0, character: 0 }, - end: { line: 1_000_000, character: 0 }, -}; - -test('unused import', async () => { - const code = ` -// @filename: test1.py -//// from test2 import [|/*marker*/foo|] - -// @filename: test2.py -//// def foo(): pass - `; - - const state = parseAndGetTestState(code).state; - - state.verifyDiagnostics({ - marker: { category: 'unused', message: '"foo" is not accessed' }, - }); -}); - -test('viper ignore unused import', async () => { - const code = ` -// @filename: test1.py -//// from test2 import [|/*marker*/foo|] # viper: ignore - -// @filename: test2.py -//// def foo(): pass - `; - - const state = parseAndGetTestState(code).state; - - state.verifyDiagnostics({ - marker: { category: 'none', message: '' }, - }); -}); - -test('getDiagnosticsForRangeWithoutFileIgnore analyzes the file on demand when its cache is empty', () => { - // Regression for the foreground/background Program split: in the product the published - // diagnostics are produced by the background-analysis Program, so the foreground Program - // that serves code-action requests may never have checked the file, leaving its pre-ignore - // diagnostics cache empty. The getter must populate that cache on demand. - const code = ` -// @filename: viperconfig.json -//// { -//// "ignore": ["**/test.py"] -//// } - -// @filename: test.py -//// /*marker*/undefinedSymbol - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker'); - state.openFile(marker.fileName); - - // Intentionally do NOT analyze: this mirrors a foreground Program that has not checked the - // file. The file is ignored, so its published diagnostics are empty regardless. - assert.strictEqual(state.program.getDiagnosticsForRange(marker.fileUri, wholeFileRange).length, 0); - - // The pre-ignore getter should still surface the unknown-symbol diagnostic by analyzing the - // file on demand. Without the on-demand analysis this returns [] and the quick fixes vanish. - const preIgnoreDiagnostics = state.program.getDiagnosticsForRangeWithoutFileIgnore(marker.fileUri, wholeFileRange); - assert( - preIgnoreDiagnostics.some((diag) => diag.getRule() === DiagnosticRule.reportUndefinedVariable), - `Expected a reportUndefinedVariable diagnostic, got ${JSON.stringify( - preIgnoreDiagnostics.map((diag) => ({ rule: diag.getRule(), message: diag.message })) - )}` - ); -}); diff --git a/packages/vipeye-internal/src/tests/documentSymbolCollector.test.ts b/packages/vipeye-internal/src/tests/documentSymbolCollector.test.ts deleted file mode 100644 index ecb474f7..00000000 --- a/packages/vipeye-internal/src/tests/documentSymbolCollector.test.ts +++ /dev/null @@ -1,399 +0,0 @@ -/* - * documentSymbolCollector.test.ts - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * - * Tests documentSymbolCollector - */ - -import { parseAndGetTestState } from './harness/fourslash/testState'; -import { verifyReferencesAtPosition } from './testStateUtils'; - -test('folder reference', () => { - const code = ` -// @filename: common/__init__.py -//// from [|io2|] import tools as tools -//// from [|io2|].tools import pathUtils as pathUtils - -// @filename: io2/empty.py -//// # empty - -// @filename: io2/tools/__init__.py -//// def combine(a, b): -//// pass - -// @filename: io2/tools/pathUtils.py -//// def getFilename(path): -//// pass - -// @filename: test1.py -//// from common import * -//// -//// tools.combine(1, 1) -//// pathUtils.getFilename("c") - -// @filename: test2.py -//// from .[|io2|] import tools as t -//// -//// t.combine(1, 1) - -// @filename: test3.py -//// from .[|io2|].tools import pathUtils as p -//// -//// p.getFilename("c") - -// @filename: test4.py -//// from common import tools, pathUtils -//// -//// tools.combine(1, 1) -//// pathUtils.getFilename("c") - -// @filename: test5.py -//// from [|io2|] import tools as tools -//// from [|io2|].tools import pathUtils as pathUtils -//// -//// tools.combine(1, 1) -//// pathUtils.getFilename("c") - `; - - const state = parseAndGetTestState(code).state; - - const ranges = state.getRangesByText().get('io2')!; - for (const range of ranges) { - verifyReferencesAtPosition(state.program, state.configOptions, 'io2', range.fileName, range.pos, ranges); - } -}); - -test('__init__ wildcard import', () => { - const code = ` -// @filename: common/__init__.py -//// from io2 import [|tools|] as [|tools|] -//// from io2.[|tools|] import pathUtils as pathUtils - -// @filename: io2/empty.py -//// # empty - -// @filename: io2/tools/__init__.py -//// def combine(a, b): -//// pass - -// @filename: io2/tools/pathUtils.py -//// def getFilename(path): -//// pass - -// @filename: test1.py -//// from common import * -//// -//// [|tools|].combine(1, 1) -//// pathUtils.getFilename("c") - -// @filename: test2.py -//// from .io2 import [|tools|] as t -//// -//// t.combine(1, 1) - -// @filename: test3.py -//// from .io2.[|tools|] import pathUtils as p -//// -//// p.getFilename("c") - -// @filename: test4.py -//// from common import [|tools|], pathUtils -//// -//// [|tools|].combine(1, 1) -//// pathUtils.getFilename("c") - -// @filename: test5.py -//// from io2 import [|tools|] as [|tools|] -//// from io2.[|tools|] import pathUtils as pathUtils -//// -//// [|tools|].combine(1, 1) -//// pathUtils.getFilename("c") - `; - - const state = parseAndGetTestState(code).state; - - const ranges = state.getRangesByText().get('tools')!; - for (const range of ranges) { - verifyReferencesAtPosition(state.program, state.configOptions, 'tools', range.fileName, range.pos, ranges); - } -}); - -test('submodule wildcard import', () => { - const code = ` -// @filename: common/__init__.py -//// from io2 import tools as tools -//// from io2.tools import [|pathUtils|] as [|pathUtils|] - -// @filename: io2/empty.py -//// # empty - -// @filename: io2/tools/__init__.py -//// def combine(a, b): -//// pass - -// @filename: io2/tools/pathUtils.py -//// def getFilename(path): -//// pass - -// @filename: test1.py -//// from common import * -//// -//// tools.combine(1, 1) -//// [|pathUtils|].getFilename("c") - -// @filename: test2.py -//// from .io2 import tools as t -//// -//// t.combine(1, 1) - -// @filename: test3.py -//// from .io2.tools import [|pathUtils|] as p -//// -//// p.getFilename("c") - -// @filename: test4.py -//// from common import tools, [|pathUtils|] -//// -//// tools.combine(1, 1) -//// [|pathUtils|].getFilename("c") - -// @filename: test5.py -//// from io2 import tools as tools -//// from io2.tools import [|pathUtils|] as [|pathUtils|] -//// -//// tools.combine(1, 1) -//// [|pathUtils|].getFilename("c") - `; - - const state = parseAndGetTestState(code).state; - - const ranges = state.getRangesByText().get('pathUtils')!; - for (const range of ranges) { - verifyReferencesAtPosition(state.program, state.configOptions, 'pathUtils', range.fileName, range.pos, ranges); - } -}); - -test('use localName import alias', () => { - const code = ` -// @filename: common/__init__.py -//// from io2 import tools as [|/*marker1*/tools|] -//// from io2.tools import pathUtils as pathUtils - -// @filename: io2/empty.py -//// # empty - -// @filename: io2/tools/__init__.py -//// def combine(a, b): -//// pass - -// @filename: io2/tools/pathUtils.py -//// def getFilename(path): -//// pass - -// @filename: test1.py -//// from common import * -//// -//// [|/*marker2*/tools|].combine(1, 1) -//// pathUtils.getFilename("c") - -// @filename: test2.py -//// from .io2 import tools as t -//// -//// t.combine(1, 1) - -// @filename: test3.py -//// from .io2.tools import pathUtils as p -//// -//// p.getFilename("c") - -// @filename: test4.py -//// from common import [|/*marker3*/tools|], pathUtils -//// -//// [|/*marker4*/tools|].combine(1, 1) -//// pathUtils.getFilename("c") - -// @filename: test5.py -//// from io2 import tools as [|/*marker5*/tools|] -//// from io2.tools import pathUtils as pathUtils -//// -//// [|/*marker6*/tools|].combine(1, 1) -//// pathUtils.getFilename("c") - `; - - const state = parseAndGetTestState(code).state; - const references = state - .getRangesByText() - .get('tools')! - .map((r) => ({ uri: r.fileUri, range: state.convertPositionRange(r) })); - - state.verifyFindAllReferences({ - marker1: { references }, - marker2: { references }, - marker3: { references }, - marker4: { references }, - marker5: { references }, - marker6: { references }, - }); -}); - -test('use localName import module', () => { - const code = ` -// @filename: common/__init__.py -//// from io2 import [|/*marker1*/tools|] as [|tools|] -//// from io2.[|/*marker2*/tools|] import pathUtils as pathUtils - -// @filename: io2/empty.py -//// # empty - -// @filename: io2/tools/__init__.py -//// def combine(a, b): -//// pass - -// @filename: io2/tools/pathUtils.py -//// def getFilename(path): -//// pass - -// @filename: test1.py -//// from common import * -//// -//// [|tools|].combine(1, 1) -//// pathUtils.getFilename("c") - -// @filename: test2.py -//// from .io2 import [|/*marker3*/tools|] as t -//// -//// t.combine(1, 1) - -// @filename: test3.py -//// from .io2.[|/*marker4*/tools|] import pathUtils as p -//// -//// p.getFilename("c") - -// @filename: test4.py -//// from common import [|tools|], pathUtils -//// -//// [|tools|].combine(1, 1) -//// pathUtils.getFilename("c") - -// @filename: test5.py -//// from io2 import [|/*marker5*/tools|] as [|tools|] -//// from io2.[|/*marker6*/tools|] import pathUtils as pathUtils -//// -//// [|tools|].combine(1, 1) -//// pathUtils.getFilename("c") - `; - - const state = parseAndGetTestState(code).state; - const references = state - .getRangesByText() - .get('tools')! - .map((r) => ({ uri: r.fileUri, range: state.convertPositionRange(r) })); - - state.verifyFindAllReferences({ - marker1: { references }, - marker2: { references }, - marker3: { references }, - marker4: { references }, - marker5: { references }, - marker6: { references }, - }); -}); - -test('import dotted name', () => { - const code = ` -// @filename: nest1/__init__.py -//// # empty - -// @filename: nest1/nest2/__init__.py -//// # empty - -// @filename: nest1/nest2/module.py -//// def foo(): -//// pass - -// @filename: test1.py -//// import [|nest1|].[|nest2|].[|module|] -//// -//// [|nest1|].[|nest2|].[|module|] - -// @filename: nest1/test2.py -//// import [|nest1|].[|nest2|].[|module|] -//// -//// [|nest1|].[|nest2|].[|module|] - `; - - const state = parseAndGetTestState(code).state; - - function verify(name: string) { - const ranges = state.getRangesByText().get(name)!; - for (const range of ranges) { - verifyReferencesAtPosition(state.program, state.configOptions, name, range.fileName, range.pos, ranges); - } - } - - verify('nest1'); - verify('nest2'); - verify('module'); -}); - -test('import alias', () => { - const code = ` -// @filename: nest/__init__.py -//// # empty - -// @filename: nest/module2.py -//// # empty - -// @filename: module1.py -//// # empty - -// @filename: test1.py -//// import [|/*marker1*/module1|] as [|module1|] - -// @filename: test2.py -//// import nest.[|/*marker2*/module2|] as [|module2|] - `; - - const state = parseAndGetTestState(code).state; - - const marker1 = state.getMarkerByName('marker1'); - const ranges1 = state.getRangesByText().get('module1')!; - verifyReferencesAtPosition( - state.program, - state.configOptions, - 'module1', - marker1.fileName, - marker1.position, - ranges1 - ); - - const marker2 = state.getMarkerByName('marker2'); - const ranges2 = state.getRangesByText().get('module2')!; - verifyReferencesAtPosition( - state.program, - state.configOptions, - 'module2', - marker2.fileName, - marker2.position, - ranges2 - ); -}); - -test('string in __all__', () => { - const code = ` -// @filename: test1.py -//// class [|/*marker1*/A|]: -//// pass -//// -//// a: "[|A|]" = "A" -//// -//// __all__ = [ "[|A|]" ] - `; - - const state = parseAndGetTestState(code).state; - - const marker1 = state.getMarkerByName('marker1'); - const ranges1 = state.getRangesByText().get('A')!; - verifyReferencesAtPosition(state.program, state.configOptions, 'A', marker1.fileName, marker1.position, ranges1); -}); diff --git a/packages/vipeye-internal/src/tests/fourslash/typings/fourslash.d.ts b/packages/vipeye-internal/src/tests/fourslash/typings/fourslash.d.ts new file mode 100644 index 00000000..ea45045f --- /dev/null +++ b/packages/vipeye-internal/src/tests/fourslash/typings/fourslash.d.ts @@ -0,0 +1,455 @@ +/* + * fourslash.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * + * Python 时代的 *.fourslash.ts 夹具与 testState.test.ts 已随 Viper 化清理删除, + * 但本文件声明的 `declare namespace _` **仍被 harness 引用**: + * packages\vipeye-internal\src\tests\harness\fourslash\testState.ts + * 使用 `_.FourSlashVerificationMode` / `_.FourSlashCompletionItem` / + * `_.FourSlashCallHierarchyItem` 等类型,删除本文件会导致 ts-jest 类型检查报 TS2694 + * (整批用例“Test suite failed to run”)。因此保留,勿删。 + * + * 其余内容与原 pyright 一致:为编辑 *.fourslash.ts 提供类型信息, + * 运行期由 harness 注入 helper(见 ...\harness\fourslash\runner.ts@runCode), + * 因此 Fourslash 类型需与 TestState 保持同步。 + * + * 保留的测试仍可参考 markup 用法: + * packages\vipeye-internal\src\tests\fourSlashParser.test.ts + */ + +declare namespace _ { + type CompletionItemKind = + | 1 + | 2 + | 3 + | 4 + | 5 + | 6 + | 7 + | 8 + | 9 + | 10 + | 11 + | 12 + | 13 + | 14 + | 15 + | 16 + | 17 + | 18 + | 19 + | 20 + | 21 + | 22 + | 23 + | 24 + | 25; + + type FourSlashVerificationMode = 'exact' | 'included' | 'excluded'; + interface FourSlashCompletionItem { + label: string; + kind: CompletionItemKind | undefined; + insertionText?: string; + documentation?: string; + detail?: string; + textEdit?: TextEdit; + additionalTextEdits?: TextEdit[]; + detailDescription?: string; + commitCharacters?: string[]; + } + + interface FourSlashCallHierarchyItem { + filePath: string; + name: string; + range: PositionRange; + } + + interface TextRange { + start: number; + length: number; + } + + interface LineAndColumn { + // Both line and column are zero-based + line: number; + column: number; + } + + interface Marker { + fileName: string; + position: number; + data?: {}; + } + + interface Range { + fileName: string; + marker?: Marker; + pos: number; + end: number; + } + + interface TextChange { + span: TextRange; + newText: string; + } + + interface Command { + title: string; + command: string; + arguments?: any[]; + } + + interface Position { + // Both line and column are zero-based + line: number; + character: number; + } + + interface PositionRange { + start: Position; + end: Position; + } + + interface TextEdit { + range: PositionRange; + newText: string; + } + + interface DocumentRange { + path: string; + range: PositionRange; + } + + interface TextEditAction { + range: PositionRange; + replacementText: string; + } + + interface FileEditAction extends TextEditAction { + filePath: string; + } + + type DocumentHighlightKind = 1 | 2 | 3; + + interface DocumentHighlight { + range: PositionRange; + kind?: DocumentHighlightKind; + } + + interface AbbreviationInfo { + importFrom?: string; + importName: string; + } + + interface TextDocumentIdentifier { + uri: string; + } + + interface OptionalVersionedTextDocumentIdentifier extends TextDocumentIdentifier { + version: number | null; + } + + interface AnnotatedTextEdit extends TextEdit { + annotationId: string; + } + + interface TextDocumentEdit { + textDocument: OptionalVersionedTextDocumentIdentifier; + edits: (TextEdit | AnnotatedTextEdit)[]; + } + + interface FileOptions { + overwrite?: boolean; + ignoreIfExists?: boolean; + } + + interface ResourceOperation { + kind: string; + annotationId?: string; + } + + interface CreateFile extends ResourceOperation { + kind: 'create'; + uri: string; + options?: FileOptions; + } + + interface RenameFile extends ResourceOperation { + kind: 'rename'; + oldUri: string; + newUri: string; + options?: FileOptions; + } + + interface DeleteFileOptions { + recursive?: boolean; + ignoreIfNotExists?: boolean; + } + + interface DeleteFile extends ResourceOperation { + kind: 'delete'; + uri: string; + options?: DeleteFileOptions; + } + + interface ChangeAnnotation { + label: string; + needsConfirmation?: boolean; + description?: string; + } + + interface WorkspaceEdit { + changes?: { + [uri: string]: TextEdit[]; + }; + documentChanges?: (TextDocumentEdit | CreateFile | RenameFile | DeleteFile)[]; + changeAnnotations?: { + [id: string]: ChangeAnnotation; + }; + } + + type MarkupKind = 'markdown' | 'plaintext'; + + type DefinitionFilter = 'all' | 'preferSource' | 'preferStubs'; + + interface Fourslash { + getMappedFilePath(path: string): string; + getDocumentHighlightKind(m?: Marker): DocumentHighlightKind | undefined; + + getMarkerName(m: Marker): string; + getMarkerByName(markerName: string): Marker; + getMarkerNames(): string[]; + getMarkers(): Marker[]; + + getRanges(): Range[]; + getRangesInFile(fileName: string): Range[]; + getRangesByText(): Map; + getFilteredRanges( + predicate: (m: Marker | undefined, d: T | undefined, text: string) => boolean + ): Range[]; + getPositionRange(markerString: string): PositionRange; + getPosition(markerString: string): Position; + get BOF(): number; + get EOF(): number; + expandPositionRange(range: PositionRange, start: number, end: number): PositionRange; + convertPositionRange(range: Range): PositionRange; + convertPathToUri(path: string): string; + getDirectoryPath(path: string): string; + getPathSep(): string; + goToBOF(): void; + goToEOF(): void; + goToPosition(positionOrLineAndColumn: number | LineAndColumn): void; + goToMarker(nameOrMarker: string | Marker): void; + goToEachMarker(markers: readonly Marker[], action: (marker: Marker, index: number) => void): void; + goToEachRange(action: (range: Range) => void): void; + goToRangeStart({ fileName, pos }: Range): void; + + select(startMarker: string, endMarker: string): void; + selectAllInFile(fileName: string): void; + selectRange(range: Range): void; + selectLine(index: number): void; + + moveCaretRight(count: number): void; + + openFile(indexOrName: number | string): void; + openFiles(indexOrNames: (number | string)[]): void; + + verifyDiagnostics(map?: { [marker: string]: { category: string; message: string | undefined } }): void; + verifyCodeActions( + verifyMode: FourSlashVerificationMode, + map: { + [marker: string]: { + codeActions: { title: string; kind: string; command?: Command; edit?: WorkspaceEdit }[]; + }; + } + ): Promise; + verifyCommand(command: Command, files: { [filePath: string]: string }): Promise; + verifyInvokeCodeAction( + map: { + [marker: string]: { title: string; files?: { [filePath: string]: string }; edits?: TextEdit[] }; + }, + verifyCodeActionCount?: boolean + ): Promise; + verifyHover(kind: string, map: { [marker: string]: string | null }): void; + verifyCompletion( + verifyMode: FourSlashVerificationMode, + docFormat: MarkupKind, + map: { + [marker: string]: { + completions: FourSlashCompletionItem[]; + memberAccessInfo?: { + lastKnownModule?: string; + lastKnownMemberName?: string; + unknownMemberName?: string; + }; + }; + }, + abbrMap?: { [abbr: string]: AbbreviationInfo } + ): Promise; + verifySignature( + docFormat: MarkupKind, + map: { + [marker: string]: { + noSig?: boolean; + signatures?: { + label: string; + parameters: string[]; + documentation?: string; + }[]; + activeParameters?: (number | undefined)[]; + callHasParameters?: boolean; + }; + } + ): void; + verifyFindAllReferences(map: { + [marker: string]: { + references: DocumentRange[]; + }; + }): void; + verifyShowCallHierarchyGetIncomingCalls(map: { + [marker: string]: { + items: FourSlashCallHierarchyItem[]; + }; + }): void; + verifyShowCallHierarchyGetOutgoingCalls(map: { + [marker: string]: { + items: FourSlashCallHierarchyItem[]; + }; + }): void; + verifyHighlightReferences(map: { + [marker: string]: { + references: DocumentHighlight[]; + }; + }): void; + verifyFindDefinitions( + map: { + [marker: string]: { + definitions: DocumentRange[]; + }; + }, + filter?: DefinitionFilter + ): void; + verifyFindTypeDefinitions(map: { + [marker: string]: { + definitions: DocumentRange[]; + }; + }): void; + verifyRename( + map: { + [marker: string]: { + newName: string; + changes: FileEditAction[]; + }; + }, + isUntitled?: boolean + ): void; + verifyTypeVerifierResults( + packageName: string, + ignoreUnknownTypesFromImports: boolean, + verboseOutput: boolean, + expected: object + ): void; + + replace(start: number, length: number, text: string): void; + + /* not tested yet + paste(text: string): void; + type(text: string): void; + deleteChar(count: number): void; + deleteLineRange(startIndex: number, endIndexInclusive: number): void; + deleteCharBehindMarker(count: number): void; + + verifyCaretAtMarker(markerName: string): void; + verifyCurrentLineContent(text: string): void; + verifyCurrentFileContent(text: string): void; + verifyTextAtCaretIs(text: string): void; + verifyRangeIs(expectedText: string, includeWhiteSpace?: boolean): void; + + setCancelled(numberOfCalls: number): void; + resetCancelled(): void; */ + } + + export interface InlayHint { + label: string; + position?: _.Position; + kind?: Consts.InlayHintKind; + tooltip?: string; + paddingLeft?: boolean; + paddingRight?: boolean; + textEdits?: TextEdit[]; + } +} + +declare var helper: _.Fourslash; + +declare namespace Consts { + export namespace MarkupKind { + export const PlainText = 'plaintext'; + export const Markdown = 'markdown'; + } + + export namespace CodeActionKind { + export const QuickFix = 'quickfix'; + export const Refactor = 'refactor'; + } + + export enum Commands { + createTypeStub = 'viper.createtypestub', + restartServer = 'viper.restartserver', + orderImports = 'viper.organizeimports', + } + + namespace DocumentHighlightKind { + /** + * A textual occurrence. + */ + const Text: 1; + /** + * Read-access of a symbol, like reading a variable. + */ + const Read: 2; + /** + * Write-access of a symbol, like writing to a variable. + */ + const Write: 3; + } + + namespace CompletionItemKind { + const Text: 1; + const Method: 2; + const Function: 3; + const Constructor: 4; + const Field: 5; + const Variable: 6; + const Class: 7; + const Interface: 8; + const Module: 9; + const Property: 10; + const Unit: 11; + const Value: 12; + const Enum: 13; + const Keyword: 14; + const Snippet: 15; + const Color: 16; + const File: 17; + const Reference: 18; + const Folder: 19; + const EnumMember: 20; + const Constant: 21; + const Struct: 22; + const Event: 23; + const Operator: 24; + const TypeParameter: 25; + } + + namespace InlayHintKind { + const Type = 1; + const Parameter = 2; + function is(value: number): value is InlayHintKind; + } + + export type InlayHintKind = 1 | 2; + + export const IndexValueDetail = 'Index value'; +} diff --git a/packages/vipeye-internal/src/tests/importResolver.test.ts b/packages/vipeye-internal/src/tests/importResolver.test.ts deleted file mode 100644 index 76143ad9..00000000 --- a/packages/vipeye-internal/src/tests/importResolver.test.ts +++ /dev/null @@ -1,1281 +0,0 @@ -/* - * importResolver.test.ts - * - * importResolver tests. - */ - -import assert from 'assert'; - -import { Dirent, ReadStream, WriteStream } from 'fs'; -import { Disposable } from 'vscode-jsonrpc'; -import { ImportResolver } from '../analyzer/importResolver'; -import { ImportType } from '../analyzer/importResult'; -import { ConfigOptions } from '../common/configOptions'; -import { FileSystem, MkDirOptions, Stats } from '../common/fileSystem'; -import { FileWatcher, FileWatcherEventHandler } from '../common/fileWatcher'; -import { FullAccessHost } from '../common/fullAccessHost'; -import { Host } from '../common/host'; -import { lib, sitePackages, typeshedFallback } from '../common/pathConsts'; -import { combinePaths, getDirectoryPath, normalizeSlashes } from '../common/pathUtils'; -import { createFromRealFileSystem, RealTempFile } from '../common/realFileSystem'; -import { ServiceKeys } from '../common/serviceKeys'; -import { ServiceProvider } from '../common/serviceProvider'; -import { createServiceProvider } from '../common/serviceProviderExtensions'; -import { Uri } from '../common/uri/uri'; -import { UriEx } from '../common/uri/uriUtils'; -import { PartialStubService } from '../partialStubService'; -import { ViperFileSystem } from '../viperFileSystem'; -import { TestAccessHost } from './harness/testAccessHost'; -import { TestFileSystem } from './harness/vfs/filesystem'; - -const libraryRoot = combinePaths(normalizeSlashes('/'), lib, sitePackages); - -function usingTrueVenv() { - return process.env.CI_IMPORT_TEST_VENVPATH !== undefined || process.env.CI_IMPORT_TEST_PYTHONPATH !== undefined; -} - -describe('Import tests with fake venv', () => { - const tempFile = new RealTempFile(); - - afterAll(() => tempFile.dispose()); - - if (!usingTrueVenv()) { - describe('Import tests that cannot run in a true venv', () => { - test('partial stub file exists', () => { - const files = [ - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'partialStub.pyi'), - content: 'def test(): ...', - }, - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), - content: 'partial\n', - }, - { - path: combinePaths(libraryRoot, 'myLib', 'partialStub.py'), - content: 'def test(): pass', - }, - ]; - - const importResult = getImportResult(files, ['myLib', 'partialStub']); - assert(importResult.isImportFound); - assert(importResult.isStubFile); - assert.strictEqual( - 1, - importResult.resolvedUris.filter( - (f) => !f.isEmpty() && f.getFilePath() === combinePaths(libraryRoot, 'myLib', 'partialStub.pyi') - ).length - ); - }); - - test('symlinked partial stub file exists', () => { - const stubSource = combinePaths(normalizeSlashes('/'), 'wheel', 'partialStub.pyi'); - const files = [ - { - path: stubSource, - content: 'def test(): ...', - }, - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), - content: 'partial\n', - }, - { - path: combinePaths(libraryRoot, 'myLib', 'partialStub.py'), - content: 'def test(): pass', - }, - ]; - - const testFS = createTestFileSystem(files); - testFS.symlinkSync(stubSource, combinePaths(libraryRoot, 'myLib-stubs', 'partialStub.pyi')); - const fs = new ViperFileSystem(testFS); - const partialStubService = new PartialStubService(fs); - const sp = createServiceProvider(testFS, fs, partialStubService); - const configOptions = new ConfigOptions(UriEx.file('/')); - const uri = UriEx.file(files[files.length - 1].path); - const importResolver = new ImportResolver( - sp, - configOptions, - new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)]) - ); - - const importResult = importResolver.resolveImport(uri, configOptions.findExecEnvironment(uri), { - leadingDots: 0, - nameParts: ['myLib', 'partialStub'], - importedSymbols: new Set(), - }); - - assert(importResult.isImportFound); - assert(importResult.isStubFile); - assert.strictEqual( - 1, - importResult.resolvedUris.filter( - (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', 'partialStub.pyi') - ).length - ); - }); - - test('partial stub __init__ exists', () => { - const files = [ - { - path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'), - content: 'def test(): ...', - }, - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), - content: 'partial\n', - }, - { - path: combinePaths(libraryRoot, 'myLib', '__init__.py'), - content: 'def test(): pass', - }, - ]; - - const importResult = getImportResult(files, ['myLib']); - assert(importResult.isImportFound); - assert(importResult.isStubFile); - assert.strictEqual( - 1, - importResult.resolvedUris.filter( - (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') - ).length - ); - }); - - test('stub package', () => { - const files = [ - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'stub.pyi'), - content: '# empty', - }, - { - path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'), - content: '# empty', - }, - { - path: combinePaths(libraryRoot, 'myLib', 'partialStub.py'), - content: 'def test(): pass', - }, - ]; - - // If fully typed stub package exists, that wins over the real package. - const importResult = getImportResult(files, ['myLib', 'partialStub']); - assert(!importResult.isImportFound); - }); - - test('partial stub package in typing folder', () => { - const typingFolder = combinePaths(normalizeSlashes('/'), 'typing'); - const files = [ - { - path: combinePaths(typingFolder, 'myLib-stubs', '__init__.pyi'), - content: 'def test(): ...', - }, - { - path: combinePaths(typingFolder, 'myLib-stubs', 'py.typed'), - content: 'partial\n', - }, - { - path: combinePaths(libraryRoot, 'myLib', '__init__.py'), - content: 'def test(): pass', - }, - ]; - - const importResult = getImportResult(files, ['myLib'], (c) => (c.stubPath = UriEx.file(typingFolder))); - assert(importResult.isImportFound); - assert(importResult.isStubFile); - assert.strictEqual( - 1, - importResult.resolvedUris.filter( - (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') - ).length - ); - }); - - test('typeshed folder', () => { - const typeshedFolder = combinePaths(normalizeSlashes('/'), 'ts'); - const files = [ - { - path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'), - content: 'def test(): ...', - }, - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), - content: 'partial\n', - }, - { - path: combinePaths(typeshedFolder, 'stubs', 'myLibPackage', 'myLib.pyi'), - content: '# empty', - }, - { - path: combinePaths(libraryRoot, 'myLib', '__init__.py'), - content: 'def test(): pass', - }, - ]; - - // Stub packages win over typeshed. - const importResult = getImportResult( - files, - ['myLib'], - (c) => (c.typeshedPath = UriEx.file(typeshedFolder)) - ); - assert(importResult.isImportFound); - assert(importResult.isStubFile); - assert.strictEqual( - 1, - importResult.resolvedUris.filter( - (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') - ).length - ); - }); - - test('typeshed fallback folder', () => { - const files = [ - { - path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'), - content: 'def test(): ...', - }, - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), - content: 'partial\n', - }, - { - path: combinePaths('/', typeshedFallback, 'stubs', 'myLibPackage', 'myLib.pyi'), - content: '# empty', - }, - { - path: combinePaths(libraryRoot, 'myLib', '__init__.py'), - content: 'def test(): pass', - }, - ]; - - // Stub packages win over typeshed. - const importResult = getImportResult(files, ['myLib']); - assert(importResult.isImportFound); - assert(importResult.isStubFile); - assert.strictEqual( - 1, - importResult.resolvedUris.filter( - (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') - ).length - ); - }); - - test('getSourceFilesFromStub maps stdlib typeshed stubs into python search paths', () => { - const stdlibSourceRoot = combinePaths(normalizeSlashes('/'), 'python-stdlib'); - const stubFilePath = combinePaths('/', typeshedFallback, 'stdlib', 'typing.pyi'); - const sourceFilePath = combinePaths(stdlibSourceRoot, 'typing.py'); - const files = [ - { - path: stubFilePath, - content: 'class MutableMapping: ...', - }, - { - path: sourceFilePath, - content: - 'from _collections_abc import MutableMapping as _MutableMapping\nMutableMapping = _MutableMapping', - }, - { - path: combinePaths(stdlibSourceRoot, '_collections_abc.py'), - content: 'class MutableMapping: pass', - }, - { - path: combinePaths('/', 'src', 'test.py'), - content: 'x = 1', - }, - ]; - - const sp = createServiceProviderFromFiles(files); - const configOptions = new ConfigOptions(UriEx.file('/')); - const importResolver = new ImportResolver( - sp, - configOptions, - new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(stdlibSourceRoot)]) - ); - const execEnv = configOptions.findExecEnvironment(UriEx.file(combinePaths('/', 'src', 'test.py'))); - - const sourceFiles = importResolver.getSourceFilesFromStub(UriEx.file(stubFilePath), execEnv, false); - - assert.deepStrictEqual( - sourceFiles.map((uri) => uri.getFilePath()), - [sourceFilePath] - ); - }); - - test('getSourceFilesFromStub preserves stdlib source roots with configured venv', () => { - const stdlibSourceRoot = combinePaths(normalizeSlashes('/'), 'python-stdlib'); - const venvPath = combinePaths(normalizeSlashes('/'), 'venvs'); - const venvName = 'test'; - const venvSitePackages = combinePaths(venvPath, venvName, lib, sitePackages); - const stubFilePath = combinePaths('/', typeshedFallback, 'stdlib', 'typing.pyi'); - const sourceFilePath = combinePaths(stdlibSourceRoot, 'typing.py'); - const files = [ - { - path: stubFilePath, - content: 'class MutableMapping: ...', - }, - { - path: sourceFilePath, - content: - 'from _collections_abc import MutableMapping as _MutableMapping\nMutableMapping = _MutableMapping', - }, - { - path: combinePaths(stdlibSourceRoot, '_collections_abc.py'), - content: 'class MutableMapping: pass', - }, - { - path: combinePaths(venvSitePackages, 'sentinel.py'), - content: 'x = 1', - }, - { - path: combinePaths('/', 'src', 'test.py'), - content: 'x = 1', - }, - ]; - - const sp = createServiceProviderFromFiles(files); - const configOptions = new ConfigOptions(UriEx.file('/')); - configOptions.venvPath = UriEx.file(venvPath); - configOptions.venv = venvName; - configOptions.pythonPath = UriEx.file(combinePaths(venvPath, venvName, 'python')); - - const importResolver = new ImportResolver( - sp, - configOptions, - new TestAccessHost( - UriEx.file(combinePaths(venvPath, venvName)), - [UriEx.file(stdlibSourceRoot), UriEx.file(venvSitePackages)], - sp.fs() - ) - ); - const execEnv = configOptions.findExecEnvironment(UriEx.file(combinePaths('/', 'src', 'test.py'))); - - const sourceFiles = importResolver.getSourceFilesFromStub(UriEx.file(stubFilePath), execEnv, false); - - assert.deepStrictEqual( - sourceFiles.map((uri) => uri.getFilePath()), - [sourceFilePath] - ); - }); - - test('configured venv site-packages keep precedence over preserved stdlib source roots', () => { - const stdlibSourceRoot = combinePaths(normalizeSlashes('/'), 'python-stdlib'); - const venvPath = combinePaths(normalizeSlashes('/'), 'venvs'); - const venvName = 'test'; - const venvSitePackages = combinePaths(venvPath, venvName, lib, sitePackages); - const venvModulePath = combinePaths(venvSitePackages, 'shadowed.py'); - const files = [ - { - path: combinePaths(stdlibSourceRoot, 'shadowed.py'), - content: 'x = "stdlib"', - }, - { - path: venvModulePath, - content: 'x = "venv"', - }, - { - path: combinePaths('/', 'src', 'test.py'), - content: 'import shadowed', - }, - ]; - - const sp = createServiceProviderFromFiles(files); - const configOptions = new ConfigOptions(UriEx.file('/')); - configOptions.venvPath = UriEx.file(venvPath); - configOptions.venv = venvName; - configOptions.pythonPath = UriEx.file(combinePaths(venvPath, venvName, 'python')); - - const importResolver = new ImportResolver( - sp, - configOptions, - new TestAccessHost( - UriEx.file(combinePaths(venvPath, venvName)), - [UriEx.file(stdlibSourceRoot), UriEx.file(venvSitePackages)], - sp.fs() - ) - ); - const uri = UriEx.file(combinePaths('/', 'src', 'test.py')); - const execEnv = configOptions.findExecEnvironment(uri); - const importResult = importResolver.resolveImport(uri, execEnv, { - leadingDots: 0, - nameParts: ['shadowed'], - importedSymbols: new Set(), - }); - - assert(importResult.isImportFound); - assert.deepStrictEqual( - importResult.resolvedUris.map((resolvedUri) => resolvedUri.getFilePath()), - [venvModulePath] - ); - }); - - test('py.typed file', () => { - const files = [ - { - path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'), - content: 'def test(): ...', - }, - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), - content: 'partial\n', - }, - { - path: combinePaths(libraryRoot, 'myLib', '__init__.py'), - content: 'def test(): pass', - }, - { - path: combinePaths(libraryRoot, 'myLib', 'py.typed'), - content: '# typed', - }, - ]; - - // Partial stub package always overrides original package. - const importResult = getImportResult(files, ['myLib']); - assert(importResult.isImportFound); - assert(importResult.isStubFile); - }); - - test('py.typed library', () => { - const files = [ - { - path: combinePaths(libraryRoot, 'os', '__init__.py'), - content: 'def test(): ...', - }, - { - path: combinePaths(libraryRoot, 'os', 'py.typed'), - content: '', - }, - { - path: combinePaths('/', typeshedFallback, 'stubs', 'os', 'os', '__init__.pyi'), - content: '# empty', - }, - ]; - - const importResult = getImportResult(files, ['os']); - assert(importResult.isImportFound); - assert.strictEqual( - files[0].path, - importResult.resolvedUris[importResult.resolvedUris.length - 1].getFilePath() - ); - }); - - test('import side by side file sub under lib folder', () => { - const files = [ - { - path: combinePaths('/lib/site-packages/myLib', 'file1.py'), - content: 'def test1(): ...', - }, - { - path: combinePaths('/lib/site-packages/myLib', 'file2.py'), - content: 'def test2(): ...', - }, - ]; - - const importResult = getImportResult(files, ['file1']); - assert(!importResult.isImportFound); - }); - }); - - test('getModuleNameForImport library file', () => { - const files = [ - { - path: combinePaths(libraryRoot, 'myLib', 'myModule', 'file1.py'), - content: '# empty', - }, - ]; - - const moduleImportInfo = getModuleNameForImport(files); - - assert.strictEqual(moduleImportInfo.importType, ImportType.ThirdParty); - assert(!moduleImportInfo.isThirdPartyPyTypedPresent); - assert(!moduleImportInfo.isLocalTypingsFile); - }); - - test('getModuleNameForImport py.typed library file', () => { - const files = [ - { - path: combinePaths(libraryRoot, 'myLib', 'py.typed'), - content: '', - }, - { - path: combinePaths(libraryRoot, 'myLib', 'myModule', 'file1.py'), - content: '# empty', - }, - ]; - - const moduleImportInfo = getModuleNameForImport(files); - - assert.strictEqual(moduleImportInfo.importType, ImportType.ThirdParty); - assert(moduleImportInfo.isThirdPartyPyTypedPresent); - assert(!moduleImportInfo.isLocalTypingsFile); - }); - - test('import found in symlinked file', () => { - const files = [ - { - path: combinePaths('/', 'external', 'file2.py'), - content: 'def f(): pass', - }, - { - path: combinePaths('/', 'src', 'file1.py'), - content: 'import file2', - }, - ]; - - const result = getImportResult( - files, - ['file2'], - (c) => { - c.defaultExtraPaths = [UriEx.file(combinePaths('/', 'external_symlinked'))]; - }, - (importResolver, uri, configOptions) => { - const fs = importResolver.serviceProvider.fs() as ViperFileSystem; - const testFs = (fs as any).realFS as TestFileSystem; - testFs.mkdirSync(UriEx.file(combinePaths('/', 'external_symlinked'))); - testFs.symlinkSync( - combinePaths('/', 'external', 'file2.py'), - combinePaths('/', 'external_symlinked', 'file2.py') - ); - } - ); - - assert(result.isImportFound); - }); - } - - describe('Import tests that can run with or without a true venv', () => { - test('side by side files', () => { - const myFile = combinePaths('src', 'file.py'); - const files = [ - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'partialStub.pyi'), - content: 'def test(): ...', - }, - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), - content: 'partial\n', - }, - { - path: combinePaths(libraryRoot, 'myLib', 'partialStub.pyi'), - content: '# empty', - }, - { - path: combinePaths(libraryRoot, 'myLib', 'partialStub.py'), - content: 'def test(): pass', - }, - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'partialStub2.pyi'), - content: 'def test(): ...', - }, - { - path: combinePaths(libraryRoot, 'myLib', 'partialStub2.py'), - content: 'def test(): pass', - }, - { - path: myFile, - content: '# not used', - }, - ]; - - const sp = createServiceProviderFromFiles(files); - const configOptions = new ConfigOptions(UriEx.file('/')); - const importResolver = new ImportResolver( - sp, - configOptions, - new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)]) - ); - - // Stub package wins over original package (per PEP 561 rules). - const myUri = UriEx.file(myFile); - const sideBySideResult = importResolver.resolveImport(myUri, configOptions.findExecEnvironment(myUri), { - leadingDots: 0, - nameParts: ['myLib', 'partialStub'], - importedSymbols: new Set(), - }); - - assert(sideBySideResult.isImportFound); - assert(sideBySideResult.isStubFile); - - const sideBySideStubFile = UriEx.file(combinePaths(libraryRoot, 'myLib', 'partialStub.pyi')); - assert.strictEqual(1, sideBySideResult.resolvedUris.filter((f) => f.key === sideBySideStubFile.key).length); - assert.strictEqual('def test(): ...', sp.fs().readFileSync(sideBySideStubFile, 'utf8')); - - // Side by side stub doesn't completely disable partial stub. - const partialStubResult = importResolver.resolveImport(myUri, configOptions.findExecEnvironment(myUri), { - leadingDots: 0, - nameParts: ['myLib', 'partialStub2'], - importedSymbols: new Set(), - }); - - assert(partialStubResult.isImportFound); - assert(partialStubResult.isStubFile); - - const partialStubFile = UriEx.file(combinePaths(libraryRoot, 'myLib', 'partialStub2.pyi')); - assert.strictEqual(1, partialStubResult.resolvedUris.filter((f) => f.key === partialStubFile.key).length); - }); - - test('stub namespace package', () => { - const files = [ - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'stub.pyi'), - content: '# empty', - }, - { - path: combinePaths(libraryRoot, 'myLib', 'partialStub.py'), - content: 'def test(): pass', - }, - ]; - - // If fully typed stub package exists, that wins over the real package. - const importResult = getImportResult(files, ['myLib', 'partialStub']); - assert(importResult.isImportFound); - assert(!importResult.isStubFile); - assert.strictEqual( - 1, - importResult.resolvedUris.filter( - (f) => !f.isEmpty() && f.getFilePath() === combinePaths(libraryRoot, 'myLib', 'partialStub.py') - ).length - ); - }); - - test('py.typed namespace package plus stubs', () => { - const typingFolder = combinePaths(normalizeSlashes('/'), 'typing'); - const files = [ - { - path: combinePaths(typingFolder, 'myLib/core', 'foo.pyi'), - content: 'def test(): pass', - }, - { - path: combinePaths(libraryRoot, 'myLib', 'py.typed'), - content: '', - }, - { - path: combinePaths(libraryRoot, 'myLib', '__init__.py'), - content: 'def test(): pass', - }, - { - path: combinePaths(libraryRoot, 'myLib', '__init__.pyi'), - content: 'def test(): pass', - }, - ]; - - const importResult = getImportResult(files, ['myLib'], (c) => (c.stubPath = UriEx.file(typingFolder))); - assert(importResult.isImportFound); - assert(importResult.isStubFile); - assert.strictEqual( - 1, - importResult.resolvedUris.filter( - (f) => !f.isEmpty() && f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') - ).length - ); - }); - - test('stub in typing folder over partial stub package', () => { - const typingFolder = combinePaths(normalizeSlashes('/'), 'typing'); - const files = [ - { - path: combinePaths(libraryRoot, 'myLib-stubs', '__init__.pyi'), - content: 'def test(): ...', - }, - { - path: combinePaths(libraryRoot, 'myLib-stubs', 'py.typed'), - content: 'partial\n', - }, - { - path: combinePaths(typingFolder, 'myLib.pyi'), - content: '# empty', - }, - { - path: combinePaths(libraryRoot, 'myLib', '__init__.py'), - content: 'def test(): pass', - }, - ]; - - // If the package exists in typing folder, that gets picked up first. - const importResult = getImportResult(files, ['myLib'], (c) => (c.stubPath = UriEx.file(typingFolder))); - assert(importResult.isImportFound); - assert(importResult.isStubFile); - assert.strictEqual( - 0, - importResult.resolvedUris.filter( - (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') - ).length - ); - }); - - test('non py.typed library', () => { - const files = [ - { - path: combinePaths(libraryRoot, 'os', '__init__.py'), - content: 'def test(): ...', - }, - { - path: combinePaths('/', typeshedFallback, 'stubs', 'os', 'os', '__init__.pyi'), - content: '# empty', - }, - ]; - - const importResult = getImportResult(files, ['os']); - assert(importResult.isImportFound); - assert.strictEqual( - files[1].path, - importResult.resolvedUris[importResult.resolvedUris.length - 1].getFilePath() - ); - }); - - test('no empty import roots', () => { - const sp = createServiceProviderFromFiles([]); - const configOptions = new ConfigOptions(Uri.empty()); // Empty, like open-file mode. - const importResolver = new ImportResolver( - sp, - configOptions, - new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)]) - ); - importResolver.getImportRoots(configOptions.getDefaultExecEnvironment()).forEach((path) => assert(path)); - }); - - test('multiple typeshedFallback', () => { - const files = [ - { - path: combinePaths('/', typeshedFallback, 'stubs', 'aLib', 'aLib', '__init__.pyi'), - content: '# empty', - }, - { - path: combinePaths('/', typeshedFallback, 'stubs', 'bLib', 'bLib', '__init__.pyi'), - content: '# empty', - }, - ]; - - const sp = createServiceProviderFromFiles(files); - const configOptions = new ConfigOptions(Uri.empty()); // Empty, like open-file mode. - const importResolver = new ImportResolver( - sp, - configOptions, - new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)]) - ); - const importRoots = importResolver.getImportRoots(configOptions.getDefaultExecEnvironment()); - - assert.strictEqual( - 1, - importRoots.filter( - (f) => !f.isEmpty() && f.getFilePath() === combinePaths('/', typeshedFallback, 'stubs', 'aLib') - ).length - ); - assert.strictEqual( - 1, - importRoots.filter( - (f) => !f.isEmpty() && f.getFilePath() === combinePaths('/', typeshedFallback, 'stubs', 'bLib') - ).length - ); - }); - - test('import side by side file root', () => { - const files = [ - { - path: combinePaths('/', 'file1.py'), - content: 'def test1(): ...', - }, - { - path: combinePaths('/', 'file2.py'), - content: 'def test2(): ...', - }, - ]; - - const importResult = getImportResult(files, ['file1']); - assert(importResult.isImportFound); - assert.strictEqual( - 1, - importResult.resolvedUris.filter((f) => f.getFilePath() === combinePaths('/', 'file1.py')).length - ); - }); - - test('import side by side file sub folder', () => { - const files = [ - { - path: combinePaths('/test', 'file1.py'), - content: 'def test1(): ...', - }, - { - path: combinePaths('/test', 'file2.py'), - content: 'def test2(): ...', - }, - ]; - - const importResult = getImportResult(files, ['file1']); - assert(importResult.isImportFound); - assert.strictEqual( - 1, - importResult.resolvedUris.filter((f) => f.getFilePath() === combinePaths('/test', 'file1.py')).length - ); - }); - - test('import side by side file sub under src folder', () => { - const files = [ - { - path: combinePaths('/src/nested', 'file1.py'), - content: 'def test1(): ...', - }, - { - path: combinePaths('/src/nested', 'file2.py'), - content: 'def test2(): ...', - }, - ]; - - const importResult = getImportResult(files, ['file1']); - assert(importResult.isImportFound); - assert.strictEqual( - 1, - importResult.resolvedUris.filter((f) => f.getFilePath() === combinePaths('/src/nested', 'file1.py')) - .length - ); - }); - - test('import file sub under containing folder', () => { - const files = [ - { - path: combinePaths('/src/nested', 'file1.py'), - content: 'def test1(): ...', - }, - { - path: combinePaths('/src/nested/nested2', 'file2.py'), - content: 'def test2(): ...', - }, - ]; - - const importResult = getImportResult(files, ['file1']); - assert(importResult.isImportFound); - assert.strictEqual( - 1, - importResult.resolvedUris.filter((f) => f.getFilePath() === combinePaths('/src/nested', 'file1.py')) - .length - ); - }); - - test("don't walk up the root", () => { - const files = [ - { - path: combinePaths('/', 'file1.py'), - content: 'def test1(): ...', - }, - ]; - - const importResult = getImportResult(files, ['notExist'], (c) => (c.projectRoot = Uri.empty())); - assert(!importResult.isImportFound); - }); - - test('nested namespace package 1', () => { - const files = [ - { - path: combinePaths('/', 'packages1', 'a', 'b', 'c', 'd.py'), - content: 'def f(): pass', - }, - { - path: combinePaths('/', 'packages1', 'a', '__init__.py'), - content: '', - }, - { - path: combinePaths('/', 'packages2', 'a', '__init__.py'), - content: '', - }, - ]; - - const importResult = getImportResult(files, ['a', 'b', 'c', 'd'], (config) => { - config.defaultExtraPaths = [ - UriEx.file(combinePaths('/', 'packages1')), - UriEx.file(combinePaths('/', 'packages2')), - ]; - }); - assert(importResult.isImportFound); - }); - - test('nested namespace package 2', () => { - const files = [ - { - path: combinePaths('/', 'packages1', 'a', 'b', 'c', 'd.py'), - content: 'def f(): pass', - }, - { - path: combinePaths('/', 'packages1', 'a', 'b', 'c', '__init__.py'), - content: '', - }, - { - path: combinePaths('/', 'packages2', 'a', 'b', 'c', '__init__.py'), - content: '', - }, - ]; - - const importResult = getImportResult(files, ['a', 'b', 'c', 'd'], (config) => { - config.defaultExtraPaths = [ - UriEx.file(combinePaths('/', 'packages1')), - UriEx.file(combinePaths('/', 'packages2')), - ]; - }); - assert(importResult.isImportFound); - }); - - test('nested namespace package 3', () => { - const files = [ - { - path: combinePaths('/', 'packages1', 'a', 'b', 'c', 'd.py'), - content: 'def f(): pass', - }, - { - path: combinePaths('/', 'packages2', 'a', '__init__.py'), - content: '', - }, - ]; - - const importResult = getImportResult(files, ['a', 'b', 'c', 'd'], (config) => { - config.defaultExtraPaths = [ - UriEx.file(combinePaths('/', 'packages1')), - UriEx.file(combinePaths('/', 'packages2')), - ]; - }); - assert(!importResult.isImportFound); - }); - - test('nested namespace package 4', () => { - const files = [ - { - path: combinePaths('/', 'packages1', 'a', 'b', '__init__.py'), - content: '', - }, - { - path: combinePaths('/', 'packages1', 'a', 'b', 'c.py'), - content: 'def f(): pass', - }, - { - path: combinePaths('/', 'packages2', 'a', '__init__.py'), - content: '', - }, - { - path: combinePaths('/', 'packages2', 'a', 'b', '__init__.py'), - content: '', - }, - ]; - - const importResult = getImportResult(files, ['a', 'b', 'c'], (config) => { - config.defaultExtraPaths = [ - UriEx.file(combinePaths('/', 'packages1')), - UriEx.file(combinePaths('/', 'packages2')), - ]; - }); - assert(!importResult.isImportFound); - }); - - test('default workspace importing side by side file', () => { - const files = [ - { - path: combinePaths('/', 'src', 'a', 'b', 'file1.py'), - content: 'import file2', - }, - { - path: combinePaths('/', 'src', 'a', 'b', 'file2.py'), - content: 'def f(): pass', - }, - ]; - - const importResult = getImportResult(files, ['file2'], (config) => { - config.projectRoot = Uri.defaultWorkspace({ isCaseSensitive: () => true }); - }); - assert(importResult.isImportFound); - }); - - test('getModuleNameForImport user file', () => { - const files = [ - { - path: combinePaths('/', 'src', 'file1.py'), - content: '# empty', - }, - ]; - - const moduleImportInfo = getModuleNameForImport(files); - - assert.strictEqual(moduleImportInfo.importType, ImportType.Local); - assert(!moduleImportInfo.isThirdPartyPyTypedPresent); - assert(!moduleImportInfo.isLocalTypingsFile); - }); - }); - - if (usingTrueVenv()) { - describe('Import tests that have to run with a venv', () => { - test('venv can find imports', () => { - const tempFile = new RealTempFile(); - const files = [ - { - path: combinePaths('/', 'file1.py'), - content: 'import pytest', - }, - ]; - - const importResult = getImportResult(files, ['pytest']); - assert(importResult.isImportFound, `Import not found: ${importResult.importFailureInfo?.join('\n')}`); - - tempFile.dispose(); - }); - }); - } - - function getImportResult( - files: { path: string; content: string }[], - nameParts: string[], - setup?: (c: ConfigOptions) => void, - preImport?: (importResolver: ImportResolver, uri: Uri, configOptions: ConfigOptions) => void - ) { - const { importResolver, uri, configOptions } = setupImportResolver(files, setup); - - if (preImport) { - preImport(importResolver, uri, configOptions); - } - - const importResult = importResolver.resolveImport(uri, configOptions.findExecEnvironment(uri), { - leadingDots: 0, - nameParts: nameParts, - importedSymbols: new Set(), - }); - - // Add the config venvpath to the import result so we can output it on failure. - if (!importResult.isImportFound) { - importResult.importFailureInfo = importResult.importFailureInfo ?? []; - importResult.importFailureInfo.push(`venvPath: ${configOptions.venvPath}`); - } - - return importResult; - } - - function getModuleNameForImport(files: { path: string; content: string }[], setup?: (c: ConfigOptions) => void) { - const { importResolver, uri, configOptions } = setupImportResolver(files, setup); - - const moduleImportInfo = importResolver.getModuleNameForImport( - uri, - configOptions.findExecEnvironment(uri), - undefined, - /* detectPyTyped */ true - ); - - return moduleImportInfo; - } - - function setupImportResolver(files: { path: string; content: string }[], setup?: (c: ConfigOptions) => void) { - const defaultHostFactory = (sp: ServiceProvider) => - new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)]); - const defaultSetup = - setup ?? - ((c) => { - /* empty */ - }); - const defaultSpFactory = (files: { path: string; content: string }[]) => createServiceProviderFromFiles(files); - - // Use environment variables to determine how to create a host and how to modify the config options. - // These are set in the CI to test imports with different options. - let hostFactory: (sp: ServiceProvider) => Host = defaultHostFactory; - let configModifier = defaultSetup; - let spFactory = defaultSpFactory; - - if (process.env.CI_IMPORT_TEST_VENVPATH) { - configModifier = (c: ConfigOptions) => { - defaultSetup(c); - c.venvPath = UriEx.file( - process.env.CI_IMPORT_TEST_VENVPATH!, - /* isCaseSensitive */ true, - /* checkRelative */ true - ); - c.venv = process.env.CI_IMPORT_TEST_VENV; - }; - spFactory = (files: { path: string; content: string }[]) => createServiceProviderWithCombinedFs(files); - } else if (process.env.CI_IMPORT_TEST_PYTHONPATH) { - configModifier = (c: ConfigOptions) => { - defaultSetup(c); - c.pythonPath = UriEx.file( - process.env.CI_IMPORT_TEST_PYTHONPATH!, - /* isCaseSensitive */ true, - /* checkRelative */ true - ); - }; - hostFactory = (sp: ServiceProvider) => { - return new TruePythonTestAccessHost(sp, tempFile); - }; - spFactory = (files: { path: string; content: string }[]) => createServiceProviderWithCombinedFs(files); - } - - const sp = spFactory(files); - const configOptions = new ConfigOptions(UriEx.file('/')); - configModifier(configOptions); - - const file = files.length > 0 ? files[files.length - 1].path : combinePaths('src', 'file.py'); - if (files.length === 0) { - files.push({ - path: file, - content: '# not used', - }); - } - - const uri = UriEx.file(file); - const importResolver = new ImportResolver(sp, configOptions, hostFactory(sp)); - - return { importResolver, uri, configOptions }; - } -}); - -function createTestFileSystem(files: { path: string; content: string }[]): TestFileSystem { - const fs = new TestFileSystem(/* ignoreCase */ false, { cwd: normalizeSlashes('/') }); - - for (const file of files) { - const path = normalizeSlashes(file.path); - const dir = getDirectoryPath(path); - fs.mkdirpSync(dir); - - fs.writeFileSync(UriEx.file(path), file.content); - } - - return fs; -} - -function createServiceProviderFromFiles(files: { path: string; content: string }[]): ServiceProvider { - const testFS = createTestFileSystem(files); - const fs = new ViperFileSystem(testFS); - const partialStubService = new PartialStubService(fs); - return createServiceProvider(testFS, fs, partialStubService); -} - -function createServiceProviderWithCombinedFs(files: { path: string; content: string }[]): ServiceProvider { - const testFS = createTestFileSystem(files); - const fs = new ViperFileSystem(new CombinedFileSystem(testFS)); - const partialStubService = new PartialStubService(fs); - return createServiceProvider(testFS, fs, partialStubService); -} - -class TruePythonTestAccessHost extends FullAccessHost { - constructor(sp: ServiceProvider, tempFile: RealTempFile) { - const clone = sp.clone(); - - // Make sure the service provide in use is using a real file system and real temporary file provider. - clone.add(ServiceKeys.tempFile, tempFile); - clone.add(ServiceKeys.fs, createFromRealFileSystem(tempFile)); - super(clone); - } -} - -class CombinedFileSystem implements FileSystem { - private _realFS = createFromRealFileSystem(this._testFS); - - constructor(private _testFS: TestFileSystem) {} - - mkdirSync(path: Uri, options?: MkDirOptions | undefined): void { - this._testFS.mkdirSync(path, options); - } - - writeFileSync(path: Uri, data: string | Buffer, encoding: BufferEncoding | null): void { - this._testFS.writeFileSync(path, data, encoding); - } - - unlinkSync(path: Uri): void { - this._testFS.unlinkSync(path); - } - - rmdirSync(path: Uri): void { - this._testFS.rmdirSync(path); - } - - createFileSystemWatcher(paths: Uri[], listener: FileWatcherEventHandler): FileWatcher { - return this._testFS.createFileSystemWatcher(paths, listener); - } - - createReadStream(path: Uri): ReadStream { - return this._testFS.createReadStream(path); - } - - createWriteStream(path: Uri): WriteStream { - return this._testFS.createWriteStream(path); - } - - copyFileSync(src: Uri, dst: Uri): void { - this._testFS.copyFileSync(src, dst); - } - - existsSync(path: Uri): boolean { - return this._testFS.existsSync(path) || this._realFS.existsSync(path); - } - - chdir(path: Uri): void { - this._testFS.chdir(path); - } - - readdirEntriesSync(path: Uri): Dirent[] { - if (this._testFS.existsSync(path)) { - return this._testFS.readdirEntriesSync(path); - } - return this._realFS.readdirEntriesSync(path); - } - - readdirSync(path: Uri): string[] { - if (this._testFS.existsSync(path)) { - return this._testFS.readdirSync(path); - } - return this._realFS.readdirSync(path); - } - - readFileSync(path: Uri, encoding?: null): Buffer; - readFileSync(path: Uri, encoding: BufferEncoding): string; - readFileSync(path: Uri, encoding?: BufferEncoding | null): string | Buffer; - readFileSync(path: Uri, encoding: BufferEncoding | null = null) { - if (this._testFS.existsSync(path)) { - return this._testFS.readFileSync(path, encoding); - } - return this._realFS.readFileSync(path, encoding); - } - - statSync(path: Uri): Stats { - if (this._testFS.existsSync(path)) { - return this._testFS.statSync(path); - } - return this._realFS.statSync(path); - } - - realpathSync(path: Uri): Uri { - if (this._testFS.existsSync(path)) { - return this._testFS.realpathSync(path); - } - return this._realFS.realpathSync(path); - } - - getModulePath(): Uri { - return this._testFS.getModulePath(); - } - - readFile(path: Uri): Promise { - if (this._testFS.existsSync(path)) { - return this._testFS.readFile(path); - } - return this._realFS.readFile(path); - } - - readFileText(path: Uri, encoding?: BufferEncoding | undefined): Promise { - if (this._testFS.existsSync(path)) { - return this._testFS.readFileText(path, encoding); - } - return this._realFS.readFileText(path, encoding); - } - - realCasePath(path: Uri): Uri { - return this._testFS.realCasePath(path); - } - - isMappedUri(filepath: Uri): boolean { - return this._testFS.isMappedUri(filepath); - } - - getOriginalUri(mappedFilePath: Uri): Uri { - return this._testFS.getOriginalUri(mappedFilePath); - } - - getMappedUri(originalFilePath: Uri): Uri { - return this._testFS.getMappedUri(originalFilePath); - } - - isInZip(path: Uri): boolean { - return this._testFS.isInZip(path); - } - - mapDirectory(mappedUri: Uri, originalUri: Uri, filter?: (originalUri: Uri, fs: FileSystem) => boolean): Disposable { - return this._realFS.mapDirectory(mappedUri, originalUri, filter); - } -} diff --git a/packages/vipeye-internal/src/tests/importResolverSupport.test.ts b/packages/vipeye-internal/src/tests/importResolverSupport.test.ts deleted file mode 100644 index d4f97133..00000000 --- a/packages/vipeye-internal/src/tests/importResolverSupport.test.ts +++ /dev/null @@ -1,434 +0,0 @@ -/* - * importResolverInfrastructure.test.ts - * - * Unit tests for the extracted ImportResolver infrastructure helpers. - */ - -import assert from 'assert'; - -import { createImportResolverFileSystem } from '../analyzer/importResolverFileSystem'; -import { ImportLogger } from '../analyzer/importLogger'; -import { createDefaultTypeshedInfoProvider } from '../analyzer/typeshedInfoProvider'; -import { typeshedFallback } from '../common/pathConsts'; -import { normalizeSlashes } from '../common/pathUtils'; -import { PythonVersion } from '../common/pythonVersion'; -import { Uri } from '../common/uri/uri'; -import { TestFileSystem } from './harness/vfs/filesystem'; - -function normalizedPath(uri: Uri): string { - return normalizeSlashes(uri.getFilePath(), '/'); -} - -describe('ImportResolverFileSystem', () => { - test('readdirEntriesSync caches per-directory and is cleared by invalidateCache', () => { - const fs = new TestFileSystem(/* ignoreCase */ false, { cwd: '/' }); - fs.mkdirpSync('/dir'); - fs.writeFileSync(Uri.file('/dir/a.py', fs), ''); - - const spy = jest.spyOn(fs, 'readdirEntriesSync'); - - const cache = createImportResolverFileSystem(fs); - cache.readdirEntriesSync(Uri.file('/dir', fs)); - cache.readdirEntriesSync(Uri.file('/dir', fs)); - assert.strictEqual(spy.mock.calls.length, 1); - - cache.invalidateCache(); - cache.readdirEntriesSync(Uri.file('/dir', fs)); - assert.strictEqual(spy.mock.calls.length, 2); - - spy.mockRestore(); - }); - - test('fileExists/dirExists follow symlinks via realpath (parity with pre-refactor behavior)', () => { - const fs = new TestFileSystem(/* ignoreCase */ false, { cwd: '/' }); - fs.mkdirpSync('/realDir'); - fs.writeFileSync(Uri.file('/realFile.txt', fs), 'x'); - fs.mkdirpSync('/links'); - fs.symlinkSync('/realFile.txt', '/links/fileLink.txt'); - fs.symlinkSync('/realDir', '/links/dirLink'); - - const cache = createImportResolverFileSystem(fs); - - assert.strictEqual(cache.fileExists(Uri.file('/links/fileLink.txt', fs)), true); - assert.strictEqual(cache.fileExists(Uri.file('/links/dirLink', fs)), false); - - assert.strictEqual(cache.dirExists(Uri.file('/links/dirLink', fs)), true); - assert.strictEqual(cache.dirExists(Uri.file('/links/fileLink.txt', fs)), false); - }); - - test('fileExists/dirExists return false for missing paths and broken symlinks', () => { - const fs = new TestFileSystem(/* ignoreCase */ false, { cwd: '/' }); - fs.mkdirpSync('/links'); - fs.symlinkSync('/doesNotExist', '/links/brokenLink'); - - const cache = createImportResolverFileSystem(fs); - - assert.strictEqual(cache.fileExists(Uri.file('/missingFile', fs)), false); - assert.strictEqual(cache.dirExists(Uri.file('/missingDir', fs)), false); - - assert.strictEqual(cache.fileExists(Uri.file('/links/brokenLink', fs)), false); - assert.strictEqual(cache.dirExists(Uri.file('/links/brokenLink', fs)), false); - }); - - test('dirExists caches existence checks for root', () => { - const fs = new TestFileSystem(/* ignoreCase */ false, { cwd: '/' }); - const statSpy = jest.spyOn(fs, 'statSync'); - - const cache = createImportResolverFileSystem(fs); - const root = Uri.file('/', fs); - - assert.strictEqual(cache.dirExists(root), true); - assert.strictEqual(cache.dirExists(root), true); - - // For the root path we should consult the filesystem once and then cache the result. - assert.strictEqual(statSpy.mock.calls.length, 1); - statSpy.mockRestore(); - }); - - test('getFilesInDirectory returns files and symlinks-to-files and caches results', () => { - const fs = new TestFileSystem(/* ignoreCase */ false, { cwd: '/' }); - fs.mkdirpSync('/dir'); - fs.writeFileSync(Uri.file('/dir/a.py', fs), ''); - fs.writeFileSync(Uri.file('/realFile.txt', fs), 'x'); - fs.symlinkSync('/realFile.txt', '/dir/fileLink.txt'); - - const readdirSpy = jest.spyOn(fs, 'readdirEntriesSync'); - - const cache = createImportResolverFileSystem(fs); - const dir = Uri.file('/dir', fs); - - const files1 = cache.getFilesInDirectory(dir); - assert(files1.some((u) => normalizedPath(u) === '/dir/a.py')); - assert(files1.some((u) => normalizedPath(u) === '/dir/fileLink.txt')); - - const callsAfterFirst = readdirSpy.mock.calls.length; - - const files2 = cache.getFilesInDirectory(dir); - assert.deepStrictEqual( - files2.map((u) => normalizedPath(u)).sort(), - files1.map((u) => normalizedPath(u)).sort() - ); - - // Second call should be served from cache. - assert.strictEqual(readdirSpy.mock.calls.length, callsAfterFirst); - - readdirSpy.mockRestore(); - }); - - test('getFilesInDirectory is stale until invalidateCache', () => { - const fs = new TestFileSystem(/* ignoreCase */ false, { cwd: '/' }); - fs.mkdirpSync('/dir'); - fs.writeFileSync(Uri.file('/dir/a.py', fs), ''); - - const cache = createImportResolverFileSystem(fs); - const dir = Uri.file('/dir', fs); - - const files1 = cache - .getFilesInDirectory(dir) - .map((u) => normalizedPath(u)) - .sort(); - assert.deepStrictEqual(files1, ['/dir/a.py']); - - fs.writeFileSync(Uri.file('/dir/b.py', fs), ''); - - // Served from cache, so it won't include newly-added files. - const files2 = cache - .getFilesInDirectory(dir) - .map((u) => normalizedPath(u)) - .sort(); - assert.deepStrictEqual(files2, ['/dir/a.py']); - - cache.invalidateCache(); - const files3 = cache - .getFilesInDirectory(dir) - .map((u) => normalizedPath(u)) - .sort(); - assert.deepStrictEqual(files3, ['/dir/a.py', '/dir/b.py']); - }); - - test('getResolvableNamesInDirectory includes extensionless file names and -stubs suffix stripping', () => { - const fs = new TestFileSystem(/* ignoreCase */ false, { cwd: '/' }); - fs.mkdirpSync('/dir'); - fs.mkdirpSync('/dir/pkg-stubs'); - fs.writeFileSync(Uri.file('/dir/foo.cpython-311-x86_64-linux-gnu.so', fs), ''); - - const cache = createImportResolverFileSystem(fs); - const names = cache.getResolvableNamesInDirectory(Uri.file('/dir', fs)); - - assert(names.has('pkg-stubs')); - assert(names.has('pkg')); - assert(names.has('foo')); - }); - - test('getResolvableNamesInDirectory strips extensions (including multi-dot) and follows symlinks for file-ness', () => { - const fs = new TestFileSystem(/* ignoreCase */ false, { cwd: '/' }); - fs.mkdirpSync('/dir'); - - fs.writeFileSync(Uri.file('/dir/a.py', fs), ''); - fs.writeFileSync(Uri.file('/dir/b.pyi', fs), ''); - fs.writeFileSync(Uri.file('/dir/c.pyc', fs), ''); - fs.writeFileSync(Uri.file('/dir/foo.cpython-311-x86_64-linux-gnu.so', fs), ''); - - fs.writeFileSync(Uri.file('/real.pyi', fs), 'x'); - fs.symlinkSync('/real.pyi', '/dir/link.pyi'); - - const cache = createImportResolverFileSystem(fs); - const names = cache.getResolvableNamesInDirectory(Uri.file('/dir', fs)); - - assert(names.has('a')); - assert(names.has('b')); - assert(names.has('c')); - assert(names.has('foo')); - assert(names.has('link')); - }); - - test('readdirEntriesSync failures are swallowed and cached until invalidateCache', () => { - const fs = new TestFileSystem(/* ignoreCase */ false, { cwd: '/' }); - fs.mkdirpSync('/boom'); - - const original = fs.readdirEntriesSync.bind(fs); - const readdirSpy = jest.spyOn(fs, 'readdirEntriesSync').mockImplementation((uri) => { - if (normalizedPath(uri) === '/boom') { - throw new Error('boom'); - } - return original(uri); - }); - - const cache = createImportResolverFileSystem(fs); - - assert.deepStrictEqual(cache.readdirEntriesSync(Uri.file('/boom', fs)), []); - assert.deepStrictEqual(cache.readdirEntriesSync(Uri.file('/boom', fs)), []); - - // Only one underlying call, and then we serve the cached empty value. - assert.strictEqual(readdirSpy.mock.calls.length, 1); - - cache.invalidateCache(); - assert.deepStrictEqual(cache.readdirEntriesSync(Uri.file('/boom', fs)), []); - assert.strictEqual(readdirSpy.mock.calls.length, 2); - - readdirSpy.mockRestore(); - }); -}); - -describe('TypeshedInfoProvider (default)', () => { - function createFsWithTypeshedLayout() { - const fs = new TestFileSystem(/* ignoreCase */ false, { cwd: '/' }); - - const typeshedRoot = `/${typeshedFallback}`; - fs.mkdirpSync(typeshedRoot); - fs.mkdirpSync(`${typeshedRoot}/stdlib`); - fs.mkdirpSync(`${typeshedRoot}/stubs`); - - const cache = createImportResolverFileSystem(fs); - const provider = createDefaultTypeshedInfoProvider(cache); - - return { fs, cache, provider, typeshedRoot }; - } - - test('getTypeshedRoot prefers custom path when present, otherwise falls back', () => { - const { fs, provider, typeshedRoot } = createFsWithTypeshedLayout(); - - fs.mkdirpSync('/customTypeshed'); - - const custom = Uri.file('/customTypeshed', fs); - const resolvedCustom = provider.getTypeshedRoot(custom); - assert(resolvedCustom); - assert.strictEqual(normalizedPath(resolvedCustom), '/customTypeshed'); - - const resolvedFallback = provider.getTypeshedRoot(undefined); - assert(resolvedFallback); - assert.strictEqual(normalizedPath(resolvedFallback), typeshedRoot); - }); - - test('getTypeshedRoot ignores a custom typeshed path that is not a directory', () => { - const { fs, provider, typeshedRoot } = createFsWithTypeshedLayout(); - - const customPath = Uri.file('/customTypeshed', fs); - fs.writeFileSync(customPath, 'not a directory'); - - const resolved = provider.getTypeshedRoot(customPath); - assert(resolved); - assert.strictEqual(normalizedPath(resolved), typeshedRoot); - }); - - test('getTypeshedSubdirectory returns undefined when the expected subdirectory does not exist', () => { - const fs = new TestFileSystem(/* ignoreCase */ false, { cwd: '/' }); - - const typeshedRoot = `/${typeshedFallback}`; - fs.mkdirpSync(typeshedRoot); - fs.mkdirpSync(`${typeshedRoot}/stdlib`); - // Intentionally omit `${typeshedRoot}/stubs`. - - const cache = createImportResolverFileSystem(fs); - const provider = createDefaultTypeshedInfoProvider(cache); - - assert.strictEqual( - provider.getTypeshedSubdirectory(/* isStdLib */ false, /* customTypeshedPath */ undefined), - undefined - ); - }); - - test('getThirdPartyPackageMap builds expected map and is memoized (independent of filesystem cache)', () => { - const { fs, cache, provider, typeshedRoot } = createFsWithTypeshedLayout(); - - // Build a minimal stubs layout: - // stubs/a/foo/ - // stubs/a/bar.pyi - // stubs/a/@python2/ (ignored) - // stubs/b/foo/ - fs.mkdirpSync(`${typeshedRoot}/stubs/a/foo`); - fs.mkdirpSync(`${typeshedRoot}/stubs/a/@python2`); - fs.writeFileSync(Uri.file(`${typeshedRoot}/stubs/a/bar.pyi`, fs), ''); - fs.writeFileSync(Uri.file(`${typeshedRoot}/stubs/a/readme.txt`, fs), ''); - fs.mkdirpSync(`${typeshedRoot}/stubs/b/foo`); - fs.writeFileSync(Uri.file(`${typeshedRoot}/stubs/outerFile.txt`, fs), ''); - - const readdirSpy = jest.spyOn(fs, 'readdirEntriesSync'); - - const [packageMap, roots] = provider.getThirdPartyPackageMap(undefined); - assert.deepStrictEqual( - roots.map((u) => normalizedPath(u)).sort(), - [`${typeshedRoot}/stubs/a`, `${typeshedRoot}/stubs/b`].sort() - ); - - const fooPaths = packageMap.get('foo'); - assert(fooPaths); - assert.deepStrictEqual( - fooPaths.map((u) => normalizedPath(u)).sort(), - [`${typeshedRoot}/stubs/a`, `${typeshedRoot}/stubs/b`].sort() - ); - - const barPaths = packageMap.get('bar'); - assert(barPaths); - assert.deepStrictEqual( - barPaths.map((u) => normalizedPath(u)), - [`${typeshedRoot}/stubs/a`] - ); - - assert(!packageMap.has('@python2')); - assert(!packageMap.has('readme')); - - const callsAfterFirst = readdirSpy.mock.calls.length; - - // Clear the ImportResolverFileSystem cache to ensure TypeshedInfoProvider memoization is effective. - cache.invalidateCache(); - - const [packageMap2, roots2] = provider.getThirdPartyPackageMap(undefined); - assert.strictEqual(packageMap2, packageMap); - assert.strictEqual(roots2, roots); - - // Should not have re-enumerated directories. - assert.strictEqual(readdirSpy.mock.calls.length, callsAfterFirst); - readdirSpy.mockRestore(); - }); - - test('getStdLibModuleVersionInfo parses VERSIONS and is memoized (independent of filesystem cache)', () => { - const { fs, cache, provider, typeshedRoot } = createFsWithTypeshedLayout(); - - const versionsUri = Uri.file(`${typeshedRoot}/stdlib/VERSIONS`, fs); - fs.writeFileSync( - versionsUri, - [ - 'asyncio: 3.4-', - 'distutils: 3.0-3.10 ; platforms=win32, !linux', - 'bar: -3.8', - 'email: 3.6+ # plus suffix is allowed', - 'this is not valid', - ': 3.7- # missing module name', - ].join('\n'), - 'utf8' - ); - - const statSpy = jest.spyOn(fs, 'statSync'); - const readSpy = jest.spyOn(fs, 'readFileSync'); - - const versionInfo = provider.getStdLibModuleVersionInfo(undefined); - - const asyncio = versionInfo.get('asyncio'); - assert(asyncio); - assert(PythonVersion.isEqualTo(asyncio.min, PythonVersion.fromString('3.4')!)); - assert.strictEqual(asyncio.max, undefined); - - const distutils = versionInfo.get('distutils'); - assert(distutils); - assert(PythonVersion.isEqualTo(distutils.min, PythonVersion.fromString('3.0')!)); - assert(PythonVersion.isEqualTo(distutils.max!, PythonVersion.fromString('3.10')!)); - assert.deepStrictEqual(distutils.supportedPlatforms, ['win32']); - assert.deepStrictEqual(distutils.unsupportedPlatforms, ['linux']); - - const bar = versionInfo.get('bar'); - assert(bar); - // Empty min version defaults to 3.0. - assert(PythonVersion.isEqualTo(bar.min, PythonVersion.fromString('3.0')!)); - assert(PythonVersion.isEqualTo(bar.max!, PythonVersion.fromString('3.8')!)); - - const email = versionInfo.get('email'); - assert(email); - assert(PythonVersion.isEqualTo(email.min, PythonVersion.fromString('3.6')!)); - - const statCallsAfterFirst = statSpy.mock.calls.length; - const readCallsAfterFirst = readSpy.mock.calls.length; - - // Clear the ImportResolverFileSystem cache to ensure TypeshedInfoProvider memoization is effective. - cache.invalidateCache(); - - const versionInfo2 = provider.getStdLibModuleVersionInfo(undefined); - assert.strictEqual(versionInfo2, versionInfo); - - assert.strictEqual(statSpy.mock.calls.length, statCallsAfterFirst); - assert.strictEqual(readSpy.mock.calls.length, readCallsAfterFirst); - - statSpy.mockRestore(); - readSpy.mockRestore(); - }); - - test('getStdLibModuleVersionInfo memoizes an empty result when VERSIONS is missing', () => { - const { fs, provider, typeshedRoot } = createFsWithTypeshedLayout(); - - // No VERSIONS file. - const versionInfo1 = provider.getStdLibModuleVersionInfo(undefined); - assert.strictEqual(versionInfo1.size, 0); - - // Create it later; memoization should keep returning the originally computed value. - fs.writeFileSync(Uri.file(`${typeshedRoot}/stdlib/VERSIONS`, fs), 'asyncio: 3.4-', 'utf8'); - const versionInfo2 = provider.getStdLibModuleVersionInfo(undefined); - assert.strictEqual(versionInfo2, versionInfo1); - assert.strictEqual(versionInfo2.size, 0); - }); - - test('getStdLibModuleVersionInfo logs and skips reading when VERSIONS is too large', () => { - const { fs, provider, typeshedRoot } = createFsWithTypeshedLayout(); - - const versionsUri = Uri.file(`${typeshedRoot}/stdlib/VERSIONS`, fs); - fs.writeFileSync(versionsUri, 'a'.repeat(256 * 1024), 'utf8'); - - const importLogger = new ImportLogger(); - const readSpy = jest.spyOn(fs, 'readFileSync'); - - const versionInfo = provider.getStdLibModuleVersionInfo(undefined, importLogger); - assert.strictEqual(versionInfo.size, 0); - assert.strictEqual(readSpy.mock.calls.length, 0); - - assert(importLogger.getLogs().some((l) => l.includes('unexpectedly large'))); - - readSpy.mockRestore(); - }); - - test('getStdLibModuleVersionInfo logs and returns empty when reading VERSIONS throws', () => { - const { fs, provider, typeshedRoot } = createFsWithTypeshedLayout(); - - const versionsUri = Uri.file(`${typeshedRoot}/stdlib/VERSIONS`, fs); - fs.writeFileSync(versionsUri, 'asyncio: 3.4-', 'utf8'); - - const importLogger = new ImportLogger(); - const readSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => { - throw new Error('read failed'); - }); - - const versionInfo = provider.getStdLibModuleVersionInfo(undefined, importLogger); - assert.strictEqual(versionInfo.size, 0); - assert(importLogger.getLogs().some((l) => l.includes('Could not read typeshed stdlib VERSIONS file'))); - - readSpy.mockRestore(); - }); -}); diff --git a/packages/vipeye-internal/src/tests/importStatementUtils.test.ts b/packages/vipeye-internal/src/tests/importStatementUtils.test.ts deleted file mode 100644 index 3282a22b..00000000 --- a/packages/vipeye-internal/src/tests/importStatementUtils.test.ts +++ /dev/null @@ -1,805 +0,0 @@ -/* - * importStatementUtils.test.ts - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * - * Unit tests for importStatementUtils module. - */ - -import assert from 'assert'; - -import { isFunctionDeclaration } from '../analyzer/declaration'; -import { ImportType } from '../analyzer/importResult'; -import { - getRelativeModuleName, - getTextEditsForAutoImportInsertions, - getTextEditsForAutoImportSymbolAddition, - getTopLevelImports, - ImportNameInfo, - ImportNameWithModuleInfo, -} from '../analyzer/importStatementUtils'; -import { findNodeByOffset } from '../analyzer/parseTreeUtils'; -import { isArray } from '../common/core'; -import { TextEditAction } from '../common/editAction'; -import { convertOffsetToPosition } from '../common/positionUtils'; -import { rangesAreEqual } from '../common/textRange'; -import { applyTextEditsToString } from '../common/workspaceEditUtils'; -import { NameNode } from '../parser/parseNodes'; -import { Range } from './harness/fourslash/fourSlashTypes'; -import { parseAndGetTestState, TestState } from './harness/fourslash/testState'; - -test('getTextEditsForAutoImportInsertion - import empty', () => { - const code = ` -//// import os[|/*marker1*/{|"r":"!n!import sys"|}|] - `; - - testInsertion(code, 'marker1', [], 'sys', ImportType.BuiltIn); -}); - -test('getTextEditsForAutoImportInsertion - import', () => { - const code = ` -//// import os[|/*marker1*/{|"r":"!n!import sys"|}|] - `; - - testInsertion(code, 'marker1', {}, 'sys', ImportType.BuiltIn); -}); - -test('getTextEditsForAutoImportInsertion - import alias', () => { - const code = ` -//// import os[|/*marker1*/{|"r":"!n!import sys as s"|}|] - `; - - testInsertion(code, 'marker1', { alias: 's' }, 'sys', ImportType.BuiltIn); -}); - -test('getTextEditsForAutoImportInsertion - multiple imports', () => { - const code = ` -//// import os[|/*marker1*/{|"r":"!n!import sys"|}|] - `; - - testInsertion(code, 'marker1', [{}, {}], 'sys', ImportType.BuiltIn); -}); - -test('getTextEditsForAutoImportInsertion - multiple imports alias', () => { - const code = ` -//// import os[|/*marker1*/{|"r":"!n!import sys as s, sys as y"|}|] - `; - - testInsertion(code, 'marker1', [{ alias: 's' }, { alias: 'y' }], 'sys', ImportType.BuiltIn); -}); - -test('getTextEditsForAutoImportInsertion - multiple imports alias duplicated', () => { - const code = ` -//// import os[|/*marker1*/{|"r":"!n!import sys as s"|}|] - `; - - testInsertion(code, 'marker1', [{ alias: 's' }, { alias: 's' }], 'sys', ImportType.BuiltIn); -}); - -test('getTextEditsForAutoImportInsertion - from import', () => { - const code = ` -//// import os[|/*marker1*/{|"r":"!n!from sys import path"|}|] - `; - - testInsertion(code, 'marker1', { name: 'path' }, 'sys', ImportType.BuiltIn); -}); - -test('getTextEditsForAutoImportInsertion - from import alias', () => { - const code = ` -//// import os[|/*marker1*/{|"r":"!n!from sys import path as p"|}|] - `; - - testInsertion(code, 'marker1', { name: 'path', alias: 'p' }, 'sys', ImportType.BuiltIn); -}); - -test('getTextEditsForAutoImportInsertion - multiple from imports', () => { - const code = ` -//// import os[|/*marker1*/{|"r":"!n!from sys import meta_path, path"|}|] - `; - - testInsertion(code, 'marker1', [{ name: 'path' }, { name: 'meta_path' }], 'sys', ImportType.BuiltIn); -}); - -test('getTextEditsForAutoImportInsertion - multiple from imports with alias', () => { - const code = ` -//// import os[|/*marker1*/{|"r":"!n!from sys import meta_path as m, path as p"|}|] - `; - - testInsertion( - code, - 'marker1', - [ - { name: 'path', alias: 'p' }, - { name: 'meta_path', alias: 'm' }, - ], - 'sys', - ImportType.BuiltIn - ); -}); - -test('getTextEditsForAutoImportInsertion - multiple from imports with alias duplicated', () => { - const code = ` -//// import os[|/*marker1*/{|"r":"!n!from sys import meta_path as m, path as p"|}|] - `; - - testInsertion( - code, - 'marker1', - [ - { name: 'path', alias: 'p' }, - { name: 'meta_path', alias: 'm' }, - { name: 'path', alias: 'p' }, - ], - 'sys', - ImportType.BuiltIn - ); -}); - -test('getTextEditsForAutoImportInsertion - multiple import statements', () => { - const code = ` -//// import os[|/*marker1*/{|"r":"!n!import sys as s!n!from sys import path as p"|}|] - `; - - testInsertion(code, 'marker1', [{ alias: 's' }, { name: 'path', alias: 'p' }], 'sys', ImportType.BuiltIn); -}); - -test('getTextEditsForAutoImportInsertion - different group', () => { - const code = ` -//// import os[|/*marker1*/{|"r":"!n!!n!import sys as s!n!from sys import path as p"|}|] - `; - - testInsertion(code, 'marker1', [{ alias: 's' }, { name: 'path', alias: 'p' }], 'sys', ImportType.Local); -}); - -test('getTextEditsForAutoImportInsertion - at the top', () => { - const code = ` -//// [|/*marker1*/{|"r":"import sys as s!n!from sys import path as p!n!!n!!n!"|}|]import os - `; - - testInsertion(code, 'marker1', [{ alias: 's' }, { name: 'path', alias: 'p' }], 'sys', ImportType.BuiltIn); -}); - -test('getTextEditsForAutoImportInsertion - at top of second group', () => { - const code = ` -//// import os -//// -//// [|/*marker1*/{|"r":"from test.a import testa!n!"|}|]from test.b import testb - `; - - testInsertion(code, 'marker1', [{ name: 'testa' }], 'test.a', ImportType.Local); -}); - -test('getTextEditsForAutoImportInsertion - at the top after module doc string', () => { - const code = ` -//// ''' module doc string ''' -//// __author__ = "Software Authors Name" -//// __copyright__ = "Copyright (C) 2004 Author Name" -//// __license__ = "Public Domain" -//// __version__ = "1.0" -//// [|/*marker1*/{|"r":"import sys as s!n!from sys import path as p!n!!n!!n!"|}|]import os - `; - - testInsertion(code, 'marker1', [{ alias: 's' }, { name: 'path', alias: 'p' }], 'sys', ImportType.BuiltIn); -}); - -test('getTextEditsForAutoImportInsertion - insert before different group adds blank line', () => { - const code = ` -// @filename: mypkg/__init__.py -//// pass - -// @filename: test.py -//// [|/*range1*/{|"r":"import sys!n!!n!"|}|]from mypkg import foo -//// foo()/*marker1*/ - `; - - testInsertion(code, 'marker1', {}, 'sys', ImportType.BuiltIn); -}); - -test('getTextEditsForAutoImportInsertion - insert before unresolved import adds blank line', () => { - const code = ` -// @filename: test.py -//// [|/*range1*/{|"r":"import sys!n!!n!"|}|]import unknownpkg -//// unknownpkg./*marker1*/foo - `; - - testInsertion(code, 'marker1', {}, 'sys', ImportType.BuiltIn); -}); - -test('getTextEditsForAutoImportInsertion - insert before same group does not add blank line (local)', () => { - const code = ` -// @filename: aaa/__init__.py -//// pass - -// @filename: mypkg/__init__.py -//// pass - -// @filename: test.py -//// [|/*range1*/{|"r":"import aaa!n!"|}|]from mypkg import foo -//// foo()/*marker1*/ - `; - - testInsertion(code, 'marker1', {}, 'aaa', ImportType.Local); -}); - -test('getTextEditsForAutoImportInsertion - insert before same group does not add blank line (built-in)', () => { - const code = ` -// @filename: test.py -//// [|/*range1*/{|"r":"import sys!n!"|}|]import zlib -//// zlib./*marker1*/crc32(b'') - `; - - testInsertion(code, 'marker1', {}, 'sys', ImportType.BuiltIn); -}); - -test('getTextEditsForAutoImportInsertion - insert before local relative adds blank line', () => { - const code = ` -// @filename: mypkg/__init__.py -//// pass - -// @filename: mypkg/test.py -//// [|/*range1*/{|"r":"import sys!n!!n!"|}|]from . import foo -//// foo()/*marker1*/ - `; - - testInsertion(code, 'marker1', {}, 'sys', ImportType.BuiltIn); -}); - -test('getTextEditsForAutoImportInsertion - insert third-party before local relative adds blank line', () => { - const code = ` -// @filename: mypkg/__init__.py -//// pass - -// @filename: mypkg/foo.py -//// bar = 1 - -// @filename: mypkg/test.py -//// [|/*range1*/{|"r":"import numpy!n!!n!"|}|]from .foo import bar -//// bar/*marker1*/ - `; - - testInsertion(code, 'marker1', {}, 'numpy', ImportType.ThirdParty); -}); - -test('getTextEditsForAutoImportInsertions - new relative import grouped below absolute local import', () => { - const code = ` -// @filename: aaa.py -//// A = 1 - -// @filename: test.py -//// from aaa import A[|/*marker1*/{|"r":"!n!!n!from .mmm import M"|}|] - `; - - const module = { moduleName: 'mmm', importType: ImportType.Local, isLocalTypingsFile: false }; - testInsertions(code, 'marker1', [{ module, name: 'M', nameForImportFrom: '.mmm' }]); -}); - -test('getTextEditsForAutoImportInsertions - new relative import sorts within existing relative group', () => { - const code = ` -// @filename: pkg/__init__.py -//// pass - -// @filename: pkg/nnn.py -//// N = 1 - -// @filename: pkg/test.py -//// [|{|"r":"from .mmm import M!n!"|}|]from .nnn import N -//// N/*marker1*/ - `; - - const module = { moduleName: 'mmm', importType: ImportType.Local, isLocalTypingsFile: false }; - testInsertions(code, 'marker1', [{ module, name: 'M', nameForImportFrom: '.mmm' }]); -}); - -test('getTextEditsForAutoImportInsertions - absolute-local and relative-local batched at same anchor (applied)', () => { - // The auto-import code action inserts a single symbol at a time, so it never batches an - // absolute-local and a relative-local insert at the same anchor; that single-insert path is the - // one this PR targets and is covered by the integration tests in codeActionProvider.common.ts. - // - // This test pins down the *batched* same-anchor path (a single getTextEditsForAutoImportInsertions - // call producing two same-range edits) and asserts the final applied document text rather than - // each edit independently. Upgrading Local -> LocalRelative changes the merge key, so the two - // inserts stay as separate same-range edits instead of merging. When applyTextEditsToString - // composes two zero-width edits at the same offset, the later-applied edit lands on top, so the - // relative import renders above the absolute one here. This ordering is a pre-existing property of - // composing multiple same-anchor edits (the long-standing builtin/third-party/local batch below - // behaves the same way) and is not introduced by this PR. - const code = ` -// @filename: aaa.py -//// A = 1 - -// @filename: pkg/__init__.py -//// pass - -// @filename: pkg/mmm.py -//// M = 1 - -// @filename: pkg/test.py -//// import os/*marker1*/ - `; - - const moduleAbs = { moduleName: 'aaa', importType: ImportType.Local, isLocalTypingsFile: false }; - const moduleRel = { moduleName: 'mmm', importType: ImportType.Local, isLocalTypingsFile: false }; - const result = applyInsertions(code, 'marker1', [ - { module: moduleAbs, name: 'A' }, - { module: moduleRel, name: 'M', nameForImportFrom: '.mmm' }, - ]); - - assert.strictEqual(result, 'import os\n\nfrom .mmm import M\n\nfrom aaa import A'); -}); - -test('getTextEditsForAutoImportInsertions - absolute-local and relative-local batched at top of import-less file (applied)', () => { - // Same batched same-anchor path as above, but at a top-of-file (0,0) insertion in a file with no - // existing imports. The two same-range (0,0) edits do not merge (different import groups), and - // applyTextEditsToString again renders the later-applied (relative) edit above the absolute one. - // Input order does not affect the result: the edit list is import-group-sorted before applying. - const code = ` -//// /*marker1*/x = 1 - `; - - const moduleAbs = { moduleName: 'aaa', importType: ImportType.Local, isLocalTypingsFile: false }; - const moduleRel = { moduleName: 'mmm', importType: ImportType.Local, isLocalTypingsFile: false }; - - // Pass relative first to prove the ordering comes from the import-group sort, not input order. - const result = applyInsertions(code, 'marker1', [ - { module: moduleRel, name: 'M', nameForImportFrom: '.mmm' }, - { module: moduleAbs, name: 'A' }, - ]); - - assert.strictEqual(result, 'from .mmm import M\n\n\nfrom aaa import A\n\n\nx = 1'); -}); - -test('getTextEditsForAutoImportInsertions - mix of import and from import statements', () => { - const code = ` -//// [|/*marker1*/{|"r":"import sys as s!n!from sys import path as p!n!!n!!n!"|}|]import os - `; - - const module = { moduleName: 'sys', importType: ImportType.BuiltIn, isLocalTypingsFile: false }; - testInsertions(code, 'marker1', [ - { module, alias: 's' }, - { module, name: 'path', alias: 'p' }, - ]); -}); - -test('getTextEditsForAutoImportInsertions - multiple modules with different group', () => { - const code = ` -//// [|/*marker1*/|][|{|"r":"from sys import path as p!n!!n!!n!"|}|][|{|"r":"import numpy!n!!n!!n!"|}|][|{|"r":"from test import join!n!!n!!n!"|}|]import os - `; - - const module1 = { moduleName: 'sys', importType: ImportType.BuiltIn, isLocalTypingsFile: false }; - const module2 = { moduleName: 'numpy', importType: ImportType.ThirdParty, isLocalTypingsFile: false }; - const module3 = { moduleName: 'test', importType: ImportType.Local, isLocalTypingsFile: false }; - - testInsertions(code, 'marker1', [ - { module: module1, name: 'path', alias: 'p' }, - { module: module2 }, - { module: module3, name: 'join' }, - ]); -}); - -test('getTextEditsForAutoImportInsertions - multiple modules with existing imports', () => { - const code = ` -//// import os[|/*marker1*/|][|{|"r":"!n!from sys import path as p"|}|][|{|"r":"!n!!n!import numpy"|}|][|{|"r":"!n!!n!from test import join"|}|] - `; - - const module1 = { moduleName: 'sys', importType: ImportType.BuiltIn, isLocalTypingsFile: false }; - const module2 = { moduleName: 'numpy', importType: ImportType.ThirdParty, isLocalTypingsFile: false }; - const module3 = { moduleName: 'test', importType: ImportType.Local, isLocalTypingsFile: false }; - - testInsertions(code, 'marker1', [ - { module: module1, name: 'path', alias: 'p' }, - { module: module2 }, - { module: module3, name: 'join' }, - ]); -}); - -test('getTextEditsForAutoImportInsertions - multiple modules with same group', () => { - const code = ` -//// import os[|/*marker1*/|][|{|"r":"!n!!n!import module2!n!from module1 import path as p!n!from module3 import join"|}|] - `; - - const module1 = { moduleName: 'module1', importType: ImportType.Local, isLocalTypingsFile: false }; - const module2 = { moduleName: 'module2', importType: ImportType.Local, isLocalTypingsFile: false }; - const module3 = { moduleName: 'module3', importType: ImportType.Local, isLocalTypingsFile: false }; - - testInsertions(code, 'marker1', [ - { module: module1, name: 'path', alias: 'p' }, - { module: module2 }, - { module: module3, name: 'join' }, - ]); -}); - -test('getTextEditsForAutoImportSymbolAddition', () => { - const code = ` -//// from sys import [|/*marker1*/{|"r":"meta_path, "|}|]path - `; - - testAddition(code, 'marker1', { name: 'meta_path' }, 'sys'); -}); - -test('getTextEditsForAutoImportSymbolAddition - already exist', () => { - const code = ` -//// from sys import path[|/*marker1*/|] - `; - - testAddition(code, 'marker1', { name: 'path' }, 'sys'); -}); - -test('getTextEditsForAutoImportSymbolAddition - with alias', () => { - const code = ` -//// from sys import path[|/*marker1*/{|"r":", path as p"|}|] - `; - - testAddition(code, 'marker1', { name: 'path', alias: 'p' }, 'sys'); -}); - -test('getTextEditsForAutoImportSymbolAddition - multiple names', () => { - const code = ` -//// from sys import [|/*marker1*/{|"r":"meta_path as m, "|}|]path[|{|"r":", zoom as z"|}|] - `; - - testAddition( - code, - 'marker1', - [ - { name: 'meta_path', alias: 'm' }, - { name: 'zoom', alias: 'z' }, - ], - 'sys' - ); -}); - -test('getTextEditsForAutoImportSymbolAddition - multiple names at some spot', () => { - const code = ` -//// from sys import [|/*marker1*/{|"r":"meta_path as m, noon as n, "|}|]path - `; - - testAddition( - code, - 'marker1', - [ - { name: 'meta_path', alias: 'm' }, - { name: 'noon', alias: 'n' }, - ], - 'sys' - ); -}); - -test('getTextEditsForAutoImportSymbolAddition - wildcard', () => { - const code = ` -//// from sys import *[|/*marker1*/|] - `; - - testAddition(code, 'marker1', [{ name: 'path' }], 'sys'); -}); - -test('getRelativeModuleName - same file', () => { - const code = ` -// @filename: source.py -//// [|/*src*/|] [|/*dest*/|] - `; - - testRelativeModuleName(code, '.source'); -}); - -test('getRelativeModuleName - same file __init__', () => { - const code = ` -// @filename: common/__init__.py -//// [|/*src*/|] [|/*dest*/|] - `; - - testRelativeModuleName(code, '.'); -}); - -test('getRelativeModuleName - same folder', () => { - const code = ` -// @filename: source.py -//// [|/*src*/|] - -// @filename: dest.py -//// [|/*dest*/|] - `; - - testRelativeModuleName(code, '.dest'); -}); - -test('getRelativeModuleName - different folder move down', () => { - const code = ` -// @filename: common/source.py -//// [|/*src*/|] - -// @filename: dest.py -//// [|/*dest*/|] - `; - - testRelativeModuleName(code, '..dest'); -}); - -test('getRelativeModuleName - different folder move up', () => { - const code = ` -// @filename: source.py -//// [|/*src*/|] - -// @filename: common/dest.py -//// [|/*dest*/|] - `; - - testRelativeModuleName(code, '.common.dest'); -}); - -test('getRelativeModuleName - folder move down __init__ parent folder', () => { - const code = ` -// @filename: nest1/nest2/source.py -//// [|/*src*/|] - -// @filename: nest1/__init__.py -//// [|/*dest*/|] - `; - - testRelativeModuleName(code, '..'); -}); - -test('getRelativeModuleName - folder move down __init__ parent folder ignore folder structure', () => { - const code = ` -// @filename: nest1/nest2/source.py -//// [|/*src*/|] - -// @filename: nest1/__init__.py -//// [|/*dest*/|] - `; - - testRelativeModuleName(code, '...nest1', /*ignoreFolderStructure*/ true); -}); - -test('getRelativeModuleName - different folder move down __init__ sibling folder', () => { - const code = ` -// @filename: nest1/nest2/source.py -//// [|/*src*/|] - -// @filename: different/__init__.py -//// [|/*dest*/|] - `; - - testRelativeModuleName(code, '...different'); -}); - -test('getRelativeModuleName - different folder move up __init__', () => { - const code = ` -// @filename: source.py -//// [|/*src*/|] - -// @filename: common/__init__.py -//// [|/*dest*/|] - `; - - testRelativeModuleName(code, '.common'); -}); - -test('getRelativeModuleName - root __init__', () => { - const code = ` -// @filename: source.py -//// [|/*src*/|] - -// @filename: __init__.py -//// [|/*dest*/|] - `; - - testRelativeModuleName(code, '.'); -}); - -test('getRelativeModuleName over fake file', () => { - const code = ` -// @filename: target.py -//// [|/*dest*/|] - `; - - const state = parseAndGetTestState(code).state; - const dest = state.getMarkerByName('dest')!.fileUri; - - assert.strictEqual( - getRelativeModuleName( - state.fs, - dest.getDirectory().combinePaths('source.py'), - dest, - state.configOptions, - /*ignoreFolderStructure*/ false, - /*sourceIsFile*/ true - ), - '.target' - ); -}); - -test('getRelativeModuleName - target in stub path', () => { - const code = ` -// @filename: source.py -//// [|/*src*/|] - -// @filename: typings/library/__init__.py -//// [|/*dest*/|] - `; - - testRelativeModuleName(code, undefined); -}); - -test('getRelativeModuleName - target in typeshed path', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "typeshedPath": "my_typeshed" -//// } - -// @filename: source.py -//// [|/*src*/|] - -// @filename: my_typeshed/library/__init__.py -//// [|/*dest*/|] - `; - - testRelativeModuleName(code, undefined); -}); - -test('resolve alias of not needed file', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: myLib/__init__.py -// @library: true -//// from myLib.foo import [|/*marker*/foo|] - -// @filename: myLib/foo.py -// @library: true -//// def foo(): pass - `; - - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName('marker')!; - - const evaluator = state.workspace.service.test_program.evaluator!; - state.openFile(marker.fileName); - - const markerUri = marker.fileUri; - const parseResults = state.workspace.service.getParseResults(markerUri)!; - const nameNode = findNodeByOffset(parseResults.parserOutput.parseTree, marker.position) as NameNode; - const aliasDecls = evaluator.getDeclInfoForNameNode(nameNode)!.decls; - - // Unroot the file. we can't explicitly close the file since it will unload the file from test program. - state.workspace.service.test_program.getSourceFileInfo(markerUri)!.isOpenByClient = false; - - const unresolved = evaluator.resolveAliasDeclaration(aliasDecls[0], /*resolveLocalNames*/ false); - assert(!unresolved); - - const resolved = evaluator.resolveAliasDeclaration(aliasDecls[0], /*resolveLocalNames*/ false, { - skipFileNeededCheck: true, - }); - - assert(resolved); - assert(isFunctionDeclaration(resolved)); -}); - -function testRelativeModuleName(code: string, expected: string | undefined, ignoreFolderStructure = false) { - const state = parseAndGetTestState(code).state; - const src = state.getMarkerByName('src')!.fileUri; - const dest = state.getMarkerByName('dest')!.fileUri; - - assert.strictEqual( - getRelativeModuleName(state.fs, src, dest, state.configOptions, ignoreFolderStructure), - expected - ); -} - -function testAddition( - code: string, - markerName: string, - importNameInfo: ImportNameInfo | ImportNameInfo[], - moduleName: string -) { - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName(markerName)!; - const parseResults = state.program.getBoundSourceFile(marker!.fileUri)!.getParseResults()!; - - const importStatement = getTopLevelImports(parseResults.parserOutput.parseTree).orderedImports.find( - (i) => i.moduleName === moduleName - )!; - const edits = getTextEditsForAutoImportSymbolAddition(importNameInfo, importStatement, parseResults); - - const ranges = [...state.getRanges().filter((r) => !!r.marker?.data)]; - assert.strictEqual(edits.length, ranges.length, `${markerName} expects ${ranges.length} but got ${edits.length}`); - - testTextEdits(state, edits, ranges); -} - -function testInsertions( - code: string, - markerName: string, - importNameInfo: ImportNameWithModuleInfo | ImportNameWithModuleInfo[] -) { - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName(markerName)!; - const parseResults = state.program.getBoundSourceFile(marker!.fileUri)!.getParseResults()!; - - const importStatements = getTopLevelImports(parseResults.parserOutput.parseTree); - const edits = getTextEditsForAutoImportInsertions( - importNameInfo, - importStatements, - parseResults, - convertOffsetToPosition(marker.position, parseResults.tokenizerOutput.lines) - ); - - const ranges = [...state.getRanges().filter((r) => !!r.marker?.data)]; - assert.strictEqual(edits.length, ranges.length, `${markerName} expects ${ranges.length} but got ${edits.length}`); - - testTextEdits(state, edits, ranges); -} - -// Computes auto-import insertion edits the same way `testInsertions` does, but actually -// applies them to the document and returns the resulting text. `testTextEdits` only checks -// each edit's range/text independently (order-insensitive), so it cannot observe how multiple -// edits at the *same* anchor compose once applied. Use this when the asserted behavior depends -// on the final applied document text. -function applyInsertions( - code: string, - markerName: string, - importNameInfo: ImportNameWithModuleInfo | ImportNameWithModuleInfo[] -): string { - const state = parseAndGetTestState(code).state; - const marker = state.getMarkerByName(markerName)!; - const sourceFile = state.program.getBoundSourceFile(marker.fileUri)!; - const parseResults = sourceFile.getParseResults()!; - - const importStatements = getTopLevelImports(parseResults.parserOutput.parseTree); - const edits = getTextEditsForAutoImportInsertions( - importNameInfo, - importStatements, - parseResults, - convertOffsetToPosition(marker.position, parseResults.tokenizerOutput.lines) - ); - - return applyTextEditsToString(edits, parseResults.tokenizerOutput.lines, parseResults.text); -} - -function testInsertion( - code: string, - markerName: string, - importNameInfo: ImportNameInfo | ImportNameInfo[], - moduleName: string, - importType: ImportType -) { - importNameInfo = isArray(importNameInfo) ? importNameInfo : [importNameInfo]; - if (importNameInfo.length === 0) { - importNameInfo.push({}); - } - - testInsertions( - code, - markerName, - importNameInfo.map((i) => { - return { - module: { - moduleName, - importType, - isLocalTypingsFile: false, - }, - name: i.name, - alias: i.alias, - }; - }) - ); -} - -function testTextEdits(state: TestState, edits: TextEditAction[], ranges: Range[]) { - for (const edit of edits) { - assert( - ranges.some((r) => { - const data = r.marker!.data as { r: string }; - const expectedText = data.r; - return ( - rangesAreEqual(state.convertPositionRange(r), edit.range) && - expectedText.replace(/!n!/g, '\n') === edit.replacementText - ); - }), - `can't find '${edit.replacementText}'@'${edit.range.start.line},${edit.range.start.character}'` - ); - } -} diff --git a/packages/vipeye-internal/src/tests/languageServer.test.ts b/packages/vipeye-internal/src/tests/languageServer.test.ts deleted file mode 100644 index 9c7d418e..00000000 --- a/packages/vipeye-internal/src/tests/languageServer.test.ts +++ /dev/null @@ -1,450 +0,0 @@ -/* - * languageServer.test.ts - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * - * Tests to verify Vipeye works as the backend for a language server. - */ - -import assert from 'assert'; -import { - CancellationToken, - CompletionItem, - CompletionRequest, - ConfigurationItem, - DidChangeWorkspaceFoldersNotification, - DidCloseTextDocumentNotification, - DocumentDiagnosticRequest, - InitializedNotification, - InitializeRequest, - MarkupContent, -} from 'vscode-languageserver'; - -import { convertOffsetToPosition } from '../common/positionUtils'; -import { PythonVersion, pythonVersion3_10 } from '../common/pythonVersion'; - -import { isArray } from '../common/core'; -import { normalizeSlashes } from '../common/pathUtils'; -import { distlibFolder } from './harness/vfs/factory'; -import { - cleanupAfterAll, - DEFAULT_WORKSPACE_ROOT, - getParseResults, - hover, - openFile, - ViperServerInfo, - runViperServer, - waitForDiagnostics, -} from './lsp/languageServerTestUtils'; - -describe(`Basic language server tests`, () => { - let serverInfo: ViperServerInfo | undefined; - async function runLanguageServer( - projectRoots: string[] | string, - code: string, - callInitialize = true, - extraSettings?: { item: ConfigurationItem; value: any }[], - pythonVersion: PythonVersion = pythonVersion3_10, - supportsBackgroundThread?: boolean, - supportsPullDiagnostics?: boolean - ) { - const result = await runViperServer( - projectRoots, - code, - callInitialize, - extraSettings, - pythonVersion, - supportsBackgroundThread, - supportsPullDiagnostics - ); - serverInfo = result; - return result; - } - - afterEach(async () => { - if (serverInfo) { - await serverInfo.dispose(); - serverInfo = undefined; - } - }); - - afterAll(async () => { - await cleanupAfterAll(); - }); - - test.each([ - { name: 'capability disabled', capability: false, initFolders: 1, firstNotify: null, secondNotify: null }, - { name: '1 init, no notifications', capability: true, initFolders: 1, firstNotify: null, secondNotify: null }, - { name: '1 init, notify with 0', capability: true, initFolders: 1, firstNotify: 0, secondNotify: null }, - { name: '1 init, notify with 1', capability: true, initFolders: 1, firstNotify: 1, secondNotify: null }, - { name: '1 init, notify with 2', capability: true, initFolders: 1, firstNotify: 2, secondNotify: null }, - { name: '1 init, notify with 0 then 0', capability: true, initFolders: 1, firstNotify: 0, secondNotify: 0 }, - { name: '1 init, notify with 0 then 1', capability: true, initFolders: 1, firstNotify: 0, secondNotify: 1 }, - { name: '1 init, notify with 0 then 2', capability: true, initFolders: 1, firstNotify: 0, secondNotify: 2 }, - { name: '1 init, notify with 1 then 0', capability: true, initFolders: 1, firstNotify: 1, secondNotify: 0 }, - { name: '1 init, notify with 1 then 1', capability: true, initFolders: 1, firstNotify: 1, secondNotify: 1 }, - { name: '1 init, notify with 1 then 2', capability: true, initFolders: 1, firstNotify: 1, secondNotify: 2 }, - { name: '1 init, notify with 2 then 0', capability: true, initFolders: 1, firstNotify: 2, secondNotify: 0 }, - { name: '1 init, notify with 2 then 1', capability: true, initFolders: 1, firstNotify: 2, secondNotify: 1 }, - { name: '1 init, notify with 2 then 2', capability: true, initFolders: 1, firstNotify: 2, secondNotify: 2 }, - { name: '2 init, no notifications', capability: true, initFolders: 2, firstNotify: null, secondNotify: null }, - { name: '2 init, notify with 2', capability: true, initFolders: 2, firstNotify: 2, secondNotify: null }, - { name: '0 init, notify with 1', capability: true, initFolders: 0, firstNotify: 1, secondNotify: null }, - { name: '0 init, notify with 2', capability: true, initFolders: 0, firstNotify: 2, secondNotify: null }, - ])('workspace initialization: $name', async ({ capability, initFolders, firstNotify, secondNotify }) => { - const code = ` -// @filename: test.py -//// import [|/*marker*/os|] - `; - const info = await runLanguageServer(DEFAULT_WORKSPACE_ROOT, code, false); - const params = info.getInitializeParams(); - const folders = params.workspaceFolders!; - const folder2 = { name: 'workspace2', uri: 'file:///workspace2' }; - - params.capabilities.workspace!.workspaceFolders = capability; - if (initFolders === 0) { - params.workspaceFolders = []; - } else if (initFolders === 2) { - params.workspaceFolders = [...folders, folder2]; - } - - await info.connection.sendRequest(InitializeRequest.type, params, CancellationToken.None); - await info.connection.sendNotification(InitializedNotification.type, {}); - - const getFoldersForNotify = (count: number) => { - if (count === 0) return []; - if (count === 1) return folders; - return [...folders, folder2]; - }; - - if (firstNotify !== null) { - await info.connection.sendNotification(DidChangeWorkspaceFoldersNotification.type, { - event: { added: getFoldersForNotify(firstNotify), removed: [] }, - }); - } - if (secondNotify !== null) { - await info.connection.sendNotification(DidChangeWorkspaceFoldersNotification.type, { - event: { added: getFoldersForNotify(secondNotify), removed: [] }, - }); - } - - openFile(info, 'marker'); - const result = await hover(info, 'marker'); - assert(result && MarkupContent.is(result.contents)); - assert.strictEqual(result.contents.value, '```python\n(module) os\n```'); - }); - - test('Hover', async () => { - const code = ` -// @filename: test.py -//// import [|/*marker*/os|] - `; - const info = await runLanguageServer(DEFAULT_WORKSPACE_ROOT, code, /* callInitialize */ true); - - // Do simple hover request - openFile(info, 'marker'); - const hoverResult = await hover(info, 'marker'); - assert(hoverResult); - assert(MarkupContent.is(hoverResult.contents)); - assert.strictEqual(hoverResult.contents.value, '```python\n(module) os\n```'); - }); - test('Completions', async () => { - const code = ` -// @filename: test.py -//// import os -//// os.[|/*marker*/|] - `; - const info = await runLanguageServer(DEFAULT_WORKSPACE_ROOT, code, /* callInitialize */ true); - - // Do simple completion request - openFile(info, 'marker'); - const marker = info.testData.markerPositions.get('marker')!; - const fileUri = marker.fileUri; - const text = info.testData.files.find((d) => d.fileName === marker.fileName)!.content; - const parseResult = getParseResults(text); - const completionResult = await info.connection.sendRequest( - CompletionRequest.type, - { - textDocument: { uri: fileUri.toString() }, - position: convertOffsetToPosition(marker.position, parseResult.tokenizerOutput.lines), - }, - CancellationToken.None - ); - - assert(completionResult); - assert(!isArray(completionResult)); - - const completionItem = completionResult.items.find((i: CompletionItem) => i.label === 'path')!; - assert(completionItem); - }); - - [false, true].forEach((supportsPullDiagnostics) => { - describe(`Diagnostics ${supportsPullDiagnostics ? 'pull' : 'push'}`, () => { - // Background analysis takes longer than 5 seconds sometimes, so we need to - // increase the timeout. - jest.setTimeout(200000); - test('background thread diagnostics', async () => { - const code = ` -// @filename: root/test.py -//// from math import cos, sin -//// import sys -//// [|/*marker*/|] - `; - const settings = [ - { - item: { - scopeUri: `file://${normalizeSlashes(DEFAULT_WORKSPACE_ROOT, '/')}`, - section: 'viper.analysis', - }, - value: { - typeCheckingMode: 'strict', - diagnosticMode: 'workspace', - }, - }, - ]; - - const info = await runLanguageServer( - DEFAULT_WORKSPACE_ROOT, - code, - /* callInitialize */ true, - settings, - undefined, - /* supportsBackgroundThread */ true, - supportsPullDiagnostics - ); - - // get the file containing the marker that also contains our task list comments - await openFile(info, 'marker'); - - // Wait for the diagnostics to publish - const diagnostics = await waitForDiagnostics(info); - const diagnostic = diagnostics.find((d) => d.uri.includes('root/test.py')); - assert(diagnostic); - assert.equal(diagnostic.diagnostics.length, 6); - - // Make sure the error has a special rule - assert.equal(diagnostic.diagnostics[1].code, 'reportUnusedImport'); - assert.equal(diagnostic.diagnostics[3].code, 'reportUnusedImport'); - assert.equal(diagnostic.diagnostics[4].code, 'reportUnusedImport'); - }); - - test('background thread diagnostics open mode', async () => { - const code = ` -// @filename: root/test.py -//// from math import cos, sin -//// import sys -//// [|/*marker*/|] - `; - const settings = [ - { - item: { - scopeUri: `file://${normalizeSlashes(DEFAULT_WORKSPACE_ROOT, '/')}`, - section: 'viper.analysis', - }, - value: { - typeCheckingMode: 'strict', - }, - }, - ]; - - const info = await runLanguageServer( - DEFAULT_WORKSPACE_ROOT, - code, - /* callInitialize */ true, - settings, - undefined, - /* supportsBackgroundThread */ true, - supportsPullDiagnostics - ); - - // get the file containing the marker that also contains our task list comments - await openFile(info, 'marker'); - - // Wait for the diagnostics to publish - const diagnostics = await waitForDiagnostics(info); - const diagnostic = diagnostics.find((d) => d.uri.includes('root/test.py')); - assert(diagnostic); - const unusedImports = diagnostic.diagnostics.filter((d) => d.code === 'reportUnusedImport'); - assert.equal(unusedImports.length, 3); - }); - - test('Diagnostic severity overrides test', async () => { - const code = ` -// @filename: test.py -//// def test([|/*marker*/x|]): ... -//// -// @filename: pyproject.toml -//// - `; - const settings = [ - { - item: { - scopeUri: `file://${normalizeSlashes(DEFAULT_WORKSPACE_ROOT, '/')}`, - section: 'viper.analysis', - }, - value: { - diagnosticSeverityOverrides: { - reportUnknownParameterType: 'warning', - }, - }, - }, - ]; - - const info = await runLanguageServer( - DEFAULT_WORKSPACE_ROOT, - code, - /* callInitialize */ true, - settings, - undefined, - /* supportsBackgroundThread */ true, - supportsPullDiagnostics - ); - - // get the file containing the marker that also contains our task list comments - await openFile(info, 'marker'); - - // Wait for the diagnostics to publish - const diagnostics = await waitForDiagnostics(info); - const diagnostic = diagnostics.find((d) => d.uri.includes('test.py')); - assert(diagnostic); - - // Make sure the error has a special rule - assert.ok( - diagnostic.diagnostics.some((d) => d.code === 'reportUnknownParameterType'), - `Expected diagnostic not found. Got ${JSON.stringify(diagnostic.diagnostics)}` - ); - }); - - if (supportsPullDiagnostics) { - // Regression test: in open-files-only mode, diagnostics for a library/out-of-workspace - // file that was transiently opened (e.g. via go-to-definition) must clear once the client - // closes the file. A re-pull of the now-closed file must return an empty `full` report. - test('closed out-of-workspace file clears diagnostics on pull (open-files-only)', async () => { - const libraryPath = normalizeSlashes(`${distlibFolder.getFilePath()}/library.py`); - const code = ` -// @filename: ${libraryPath} -//// x: int = [|/*lib*/"not an int"|] - `; - const settings = [ - { - item: { - scopeUri: `file://${normalizeSlashes(DEFAULT_WORKSPACE_ROOT, '/')}`, - section: 'viper.analysis', - }, - value: { - // default open-files-only mode - diagnosticMode: 'openFilesOnly', - }, - }, - ]; - - const info = await runLanguageServer( - DEFAULT_WORKSPACE_ROOT, - code, - /* callInitialize */ true, - settings, - undefined, - /* supportsBackgroundThread */ true, - supportsPullDiagnostics - ); - - // Simulate go-to-definition opening the out-of-workspace library file. - await openFile(info, 'lib'); - const libUri = info.testData.markerPositions.get('lib')!.fileUri.toString(); - - // 1) While the library file is open, a pull reports the type error. - const reportOpen: any = await info.connection.sendRequest(DocumentDiagnosticRequest.type, { - textDocument: { uri: libUri }, - }); - assert.strictEqual(reportOpen?.kind, 'full'); - assert.ok( - (reportOpen?.items?.length ?? 0) > 0, - `expected library file to report errors while open, got ${JSON.stringify(reportOpen?.items)}` - ); - - // 2) Close the library file. - info.connection.sendNotification(DidCloseTextDocumentNotification.type, { - textDocument: { uri: libUri }, - }); - - // 3) A re-pull of the now-closed out-of-workspace file must return an empty full report. - const reportClosed: any = await info.connection.sendRequest(DocumentDiagnosticRequest.type, { - textDocument: { uri: libUri }, - }); - assert.strictEqual(reportClosed?.kind, 'full'); - assert.strictEqual( - reportClosed?.items?.length ?? 0, - 0, - `expected no diagnostics for closed out-of-workspace library file, got ${JSON.stringify( - reportClosed?.items - )}` - ); - }); - - // Regression guard: the open-state guard must only apply in - // open-files-only mode. In `workspace` mode (`checkOnlyOpenFiles === false`) a file - // that the client has closed is still analyzed, so a re-pull must continue to report - // its diagnostics. This proves the guard's `checkOnlyOpenFiles` condition is - // load-bearing and that the fix did not change workspace-mode behavior. - test('closed workspace file still reports diagnostics on pull (workspace mode)', async () => { - const code = ` -// @filename: root/test.py -//// x: int = [|/*marker*/"not an int"|] - `; - const settings = [ - { - item: { - scopeUri: `file://${normalizeSlashes(DEFAULT_WORKSPACE_ROOT, '/')}`, - section: 'viper.analysis', - }, - value: { - diagnosticMode: 'workspace', - }, - }, - ]; - - const info = await runLanguageServer( - DEFAULT_WORKSPACE_ROOT, - code, - /* callInitialize */ true, - settings, - undefined, - /* supportsBackgroundThread */ true, - supportsPullDiagnostics - ); - - await openFile(info, 'marker'); - const fileUri = info.testData.markerPositions.get('marker')!.fileUri.toString(); - - // 1) While the file is open, a pull reports the type error. - const reportOpen: any = await info.connection.sendRequest(DocumentDiagnosticRequest.type, { - textDocument: { uri: fileUri }, - }); - assert.strictEqual(reportOpen?.kind, 'full'); - assert.ok( - (reportOpen?.items?.length ?? 0) > 0, - `expected workspace file to report errors while open, got ${JSON.stringify(reportOpen?.items)}` - ); - - // 2) Close the file. - info.connection.sendNotification(DidCloseTextDocumentNotification.type, { - textDocument: { uri: fileUri }, - }); - - // 3) In workspace mode the closed file is still analyzed, so a re-pull must still - // report the error (the open-state guard must NOT fire here). - const reportClosed: any = await info.connection.sendRequest(DocumentDiagnosticRequest.type, { - textDocument: { uri: fileUri }, - }); - assert.strictEqual(reportClosed?.kind, 'full'); - assert.ok( - (reportClosed?.items?.length ?? 0) > 0, - `expected workspace-mode closed file to still report errors, got ${JSON.stringify( - reportClosed?.items - )}` - ); - }); - } - }); - }); -}); diff --git a/packages/vipeye-internal/src/tests/privateImportUsage.test.ts b/packages/vipeye-internal/src/tests/privateImportUsage.test.ts deleted file mode 100644 index 53695b5b..00000000 --- a/packages/vipeye-internal/src/tests/privateImportUsage.test.ts +++ /dev/null @@ -1,342 +0,0 @@ -/* - * privateImportUsage.test.ts - * - * Tests for reportPrivateImportUsage when source packages are added to check paths. - * - * Bug: When a py.typed package is added to check paths (e.g., via command line), - * the reportPrivateImportUsage errors incorrectly disappear because the file is - * created with isInPyTypedPackage=false instead of detecting it properly. - */ - -import assert from 'assert'; - -import { ImportResolver } from '../analyzer/importResolver'; -import { Program } from '../analyzer/program'; -import { ConfigOptions } from '../common/configOptions'; -import { DiagnosticCategory } from '../common/diagnostic'; -import { lib, sitePackages } from '../common/pathConsts'; -import { combinePaths, getDirectoryPath, normalizeSlashes } from '../common/pathUtils'; -import { createServiceProvider } from '../common/serviceProviderExtensions'; -import { UriEx } from '../common/uri/uriUtils'; -import { TestAccessHost } from './harness/testAccessHost'; -import { TestFileSystem } from './harness/vfs/filesystem'; -import { ViperFileSystem } from '../viperFileSystem'; - -const libraryRoot = combinePaths(normalizeSlashes('/'), lib, sitePackages); - -function createTestFileSystem(files: { path: string; content: string }[]): TestFileSystem { - const fs = new TestFileSystem(/* ignoreCase */ false, { cwd: normalizeSlashes('/') }); - - for (const file of files) { - const path = normalizeSlashes(file.path); - const dir = getDirectoryPath(path); - fs.mkdirpSync(dir); - - fs.writeFileSync(UriEx.file(path), file.content); - } - - return fs; -} - -function createServiceProviderFromFiles(files: { path: string; content: string }[]) { - const testFS = createTestFileSystem(files); - const fs = new ViperFileSystem(testFS); - return createServiceProvider(testFS, fs); -} - -describe('reportPrivateImportUsage with tracked library files', () => { - test('error should persist when library with py.typed is also a tracked file', () => { - // Setup: Create three packages - // pkg_a: defines helper_func - // pkg_b: imports helper_func from pkg_a but doesn't re-export it (has py.typed) - // pkg_c: imports helper_func from pkg_b (should get error) - - const files = [ - // pkg_a in library (defines the original function) - { - path: combinePaths(libraryRoot, 'pkg_a', '__init__.py'), - content: '', - }, - { - path: combinePaths(libraryRoot, 'pkg_a', 'py.typed'), - content: '', - }, - { - path: combinePaths(libraryRoot, 'pkg_a', 'utils.py'), - content: 'def helper_func(): pass', - }, - // pkg_b in library (re-imports without re-exporting) - { - path: combinePaths(libraryRoot, 'pkg_b', '__init__.py'), - content: '', - }, - { - path: combinePaths(libraryRoot, 'pkg_b', 'py.typed'), - content: '', - }, - { - path: combinePaths(libraryRoot, 'pkg_b', 'reexport.py'), - content: 'from pkg_a.utils import helper_func', // No __all__, not re-exported - }, - // pkg_c - local source file that imports from pkg_b - { - path: normalizeSlashes('/src/pkg_c/__init__.py'), - content: '', - }, - { - path: normalizeSlashes('/src/pkg_c/bad_import.py'), - content: 'from pkg_b.reexport import helper_func', // Should error - }, - ]; - - const sp = createServiceProviderFromFiles(files); - const configOptions = new ConfigOptions(UriEx.file('/')); - - // Set up execution environment with reportPrivateImportUsage enabled - configOptions.diagnosticRuleSet.reportPrivateImportUsage = 'error'; - - const importResolver = new ImportResolver( - sp, - configOptions, - new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)]) - ); - - const program = new Program(importResolver, configOptions, sp); - - // Track only the consumer file (pkg_c) - const consumerUri = UriEx.file('/src/pkg_c/bad_import.py'); - program.setTrackedFiles([consumerUri]); - - // Analyze - while (program.analyze()) { - // Continue until complete - } - - // Get diagnostics for the consumer file - const sourceFile = program.getSourceFile(consumerUri); - assert(sourceFile, 'Source file should exist'); - const diagnostics = sourceFile.getDiagnostics(configOptions) || []; - const errors = diagnostics.filter((d) => d.category === DiagnosticCategory.Error); - - // Should have 1 error about private import - assert.strictEqual( - errors.length, - 1, - `Expected 1 error when only consumer is tracked, got ${errors.length}: ${errors - .map((e) => e.message) - .join(', ')}` - ); - assert( - errors[0].message.includes('not exported') || errors[0].message.includes('helper_func'), - `Error message should mention private import: ${errors[0].message}` - ); - - program.dispose(); - - // Now create a new program and track BOTH the library file and the consumer - const program2 = new Program(importResolver, configOptions, sp); - - const libraryFileUri = UriEx.file(combinePaths(libraryRoot, 'pkg_b', 'reexport.py')); - - // Track both files - this is the bug scenario - // When the library file is tracked, it should still detect py.typed - program2.setTrackedFiles([consumerUri, libraryFileUri]); - - // Analyze - while (program2.analyze()) { - // Continue until complete - } - - // Get diagnostics for the consumer file again - const sourceFile2 = program2.getSourceFile(consumerUri); - assert(sourceFile2, 'Source file should exist in second program'); - const diagnostics2 = sourceFile2.getDiagnostics(configOptions) || []; - const errors2 = diagnostics2.filter((d) => d.category === DiagnosticCategory.Error); - - // BUG: Without the fix, this would be 0 errors instead of 1 - // The error disappears because pkg_b/reexport.py is created with isInPyTypedPackage=false - assert.strictEqual( - errors2.length, - 1, - `Expected 1 error when library is also tracked, got ${errors2.length}. ` + - `Errors: ${errors2.map((e) => e.message).join(', ')}. ` + - `This is the bug - error should persist even when library file is tracked.` - ); - - program2.dispose(); - sp.dispose(); - }); - - test('config file reportPrivateImportUsage override should apply when includeFileSpecsOverride is set', () => { - // This tests the scenario where viper is run with positional directory arguments - // (e.g., viper --project viperconfig.json dir1) and the config file has - // reportPrivateImportUsage: false. The override should still be respected. - - const files = [ - // pkg_a in library (defines the original function) - { - path: combinePaths(libraryRoot, 'pkg_a', '__init__.py'), - content: '', - }, - { - path: combinePaths(libraryRoot, 'pkg_a', 'py.typed'), - content: '', - }, - { - path: combinePaths(libraryRoot, 'pkg_a', 'utils.py'), - content: 'def helper_func(): pass', - }, - // pkg_b in library (re-imports without re-exporting) - { - path: combinePaths(libraryRoot, 'pkg_b', '__init__.py'), - content: '', - }, - { - path: combinePaths(libraryRoot, 'pkg_b', 'py.typed'), - content: '', - }, - { - path: combinePaths(libraryRoot, 'pkg_b', 'reexport.py'), - content: 'from pkg_a.utils import helper_func', // No __all__, not re-exported - }, - // Consumer package - local source file that imports from pkg_b - { - path: normalizeSlashes('/src/consumer/__init__.py'), - content: '', - }, - { - path: normalizeSlashes('/src/consumer/bad_import.py'), - content: 'from pkg_b.reexport import helper_func', // Would normally error - }, - ]; - - const sp = createServiceProviderFromFiles(files); - - // Test 1: With reportPrivateImportUsage = 'error' (default) -> should get error - { - const configOptions = new ConfigOptions(UriEx.file('/')); - configOptions.diagnosticRuleSet.reportPrivateImportUsage = 'error'; - - const importResolver = new ImportResolver( - sp, - configOptions, - new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)]) - ); - - const program = new Program(importResolver, configOptions, sp); - const consumerUri = UriEx.file('/src/consumer/bad_import.py'); - program.setTrackedFiles([consumerUri]); - - while (program.analyze()) { - // Continue until complete - } - - const sourceFile = program.getSourceFile(consumerUri); - assert(sourceFile, 'Source file should exist'); - const diagnostics = sourceFile.getDiagnostics(configOptions) || []; - const errors = diagnostics.filter((d) => d.category === DiagnosticCategory.Error); - - assert.strictEqual( - errors.length, - 1, - `Expected 1 error with reportPrivateImportUsage=error, got ${errors.length}` - ); - - program.dispose(); - } - - // Test 2: With reportPrivateImportUsage = 'none' (simulating config override) -> should NOT get error - { - const configOptions = new ConfigOptions(UriEx.file('/')); - configOptions.diagnosticRuleSet.reportPrivateImportUsage = 'none'; - - const importResolver = new ImportResolver( - sp, - configOptions, - new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)]) - ); - - const program = new Program(importResolver, configOptions, sp); - const consumerUri = UriEx.file('/src/consumer/bad_import.py'); - program.setTrackedFiles([consumerUri]); - - while (program.analyze()) { - // Continue until complete - } - - const sourceFile = program.getSourceFile(consumerUri); - assert(sourceFile, 'Source file should exist'); - const diagnostics = sourceFile.getDiagnostics(configOptions) || []; - const privateImportErrors = diagnostics.filter( - (d) => - d.category === DiagnosticCategory.Error && - (d.message.includes('not exported') || d.message.includes('helper_func')) - ); - - assert.strictEqual( - privateImportErrors.length, - 0, - `Expected 0 private import errors with reportPrivateImportUsage=none, got ${ - privateImportErrors.length - }: ${privateImportErrors.map((e) => e.message).join(', ')}` - ); - - program.dispose(); - } - - // Test 3: With reportPrivateImportUsage = 'none' AND positional arg include override - // This simulates: viper --project config.json consumer/ - // where config.json has reportPrivateImportUsage: false - { - const configOptions = new ConfigOptions(UriEx.file('/')); - // Simulate the config file setting: first set standard mode defaults - configOptions.diagnosticRuleSet = ConfigOptions.getDiagnosticRuleSet('standard'); - // Then apply the config override (reportPrivateImportUsage: false -> 'none') - configOptions.diagnosticRuleSet.reportPrivateImportUsage = 'none'; - - // Simulate includeFileSpecsOverride by changing the include - // (In the real flow, this is done by _applyCommandLineOverrides) - configOptions.include = []; - - const importResolver = new ImportResolver( - sp, - configOptions, - new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)]) - ); - - const program = new Program(importResolver, configOptions, sp); - const consumerUri = UriEx.file('/src/consumer/bad_import.py'); - - // Track only the consumer file (simulating positional arg) - program.setTrackedFiles([consumerUri]); - - while (program.analyze()) { - // Continue until complete - } - - const sourceFile = program.getSourceFile(consumerUri); - assert(sourceFile, 'Source file should exist'); - const diagnostics = sourceFile.getDiagnostics(configOptions) || []; - const privateImportErrors = diagnostics.filter( - (d) => - d.category === DiagnosticCategory.Error && - (d.message.includes('not exported') || d.message.includes('helper_func')) - ); - - // The config says reportPrivateImportUsage: false, so there should be no errors - // even when include is overridden by positional args. - assert.strictEqual( - privateImportErrors.length, - 0, - `Expected 0 private import errors with config override and include override, got ${ - privateImportErrors.length - }: ${privateImportErrors.map((e) => e.message).join(', ')}. ` + - `This would indicate the bug where positional args cause config overrides to be ignored.` - ); - - program.dispose(); - } - - sp.dispose(); - }); -}); diff --git a/packages/vipeye-internal/src/tests/service.test.ts b/packages/vipeye-internal/src/tests/service.test.ts deleted file mode 100644 index 121a7836..00000000 --- a/packages/vipeye-internal/src/tests/service.test.ts +++ /dev/null @@ -1,710 +0,0 @@ -/* - * service.test.ts - * - * service tests. - */ - -import assert from 'assert'; - -import { CancellationToken } from 'vscode-jsonrpc'; -import { InvalidatedReason } from '../analyzer/backgroundAnalysisProgram'; -import { SourceEnumerator } from '../analyzer/sourceEnumerator'; -import { IPythonMode } from '../analyzer/sourceFile'; -import { NullConsole } from '../common/console'; -import { CommandLineOptions } from '../common/commandLineOptions'; -import { combinePaths, getDirectoryPath, normalizeSlashes } from '../common/pathUtils'; -import { Uri } from '../common/uri/uri'; -import { getFileSpec, UriEx } from '../common/uri/uriUtils'; -import { TestFileSystem } from './harness/vfs/filesystem'; -import { parseTestData } from './harness/fourslash/fourSlashParser'; -import { parseAndGetTestState, TestState } from './harness/fourslash/testState'; - -test('random library file changed', () => { - const state = parseAndGetTestState('', '/projectRoot').state; - - assert.strictEqual( - state.workspace.service.test_shouldHandleLibraryFileWatchChanges( - Uri.file('/site-packages/test.py', state.serviceProvider), - [Uri.file('/site-packages', state.serviceProvider)] - ), - true - ); -}); - -test('random library file starting with . changed', () => { - const state = parseAndGetTestState('', '/projectRoot').state; - - assert.strictEqual( - state.workspace.service.test_shouldHandleLibraryFileWatchChanges( - Uri.file('/site-packages/.test.py', state.serviceProvider), - [Uri.file('/site-packages', state.serviceProvider)] - ), - false - ); -}); - -test('source enumeration reports symlinked include roots', () => { - const fs = new TestFileSystem(/* ignoreCase */ false, { cwd: '/' }); - fs.mkdirpSync('/realRoot/pkg'); - fs.writeFileSync(Uri.file('/realRoot/pkg/module.py', fs), 'x = 1'); - fs.symlinkSync('/realRoot', '/workspaceLink'); - - const enumerator = new SourceEnumerator( - [getFileSpec(Uri.file('/', fs), 'workspaceLink')], - [], - /* autoExcludeVenv */ false, - fs, - new NullConsole() - ); - - const result = enumerator.enumerate(/* timeLimitInMs */ 1000); - - assert.strictEqual(result.isComplete, true); - assert.deepStrictEqual( - enumerator.getSymlinkedDirectoryRoots().map((uri) => uri.key), - [Uri.file('/workspaceLink', fs).key] - ); -}); - -test('random library file changed, nested search paths', () => { - const state = parseAndGetTestState('', '/projectRoot').state; - - assert.strictEqual( - state.workspace.service.test_shouldHandleLibraryFileWatchChanges( - Uri.file('/lib/.venv/site-packages/myFile.py', state.serviceProvider), - [Uri.file('/lib', state.serviceProvider), Uri.file('/lib/.venv/site-packages', state.serviceProvider)] - ), - true - ); -}); - -test('random library file changed, nested search paths, fs is not case sensitive', () => { - const code = ` -// global options -// @ignoreCase: true - `; - const state = parseAndGetTestState(code, '/projectRoot').state; - - assert.strictEqual( - state.workspace.service.test_shouldHandleLibraryFileWatchChanges( - Uri.file('/lib/.venv/site-packages/myFile.py', state.serviceProvider), - [Uri.file('/lib', state.serviceProvider), Uri.file('/LIB/.venv/site-packages', state.serviceProvider)] - ), - true - ); -}); - -test('random library file changed, nested search paths, fs is case sensitive', () => { - const code = ` -// global options -// @ignoreCase: false - `; - const state = parseAndGetTestState(code, '/projectRoot').state; - - assert.strictEqual( - state.workspace.service.test_shouldHandleLibraryFileWatchChanges( - Uri.file('/lib/.venv/site-packages/myFile.py', state.serviceProvider), - [Uri.file('/lib', state.serviceProvider), Uri.file('/LIB/.venv/site-packages', state.serviceProvider)] - ), - false - ); -}); - -test('random library file starting with . changed, fs is not case sensitive', () => { - const code = ` -// global options -// @ignoreCase: true - `; - const state = parseAndGetTestState(code, '/projectRoot').state; - - assert.strictEqual( - state.workspace.service.test_shouldHandleLibraryFileWatchChanges( - Uri.file('/lib/.test.py', state.serviceProvider), - [Uri.file('/LIB', state.serviceProvider), Uri.file('/lib/site-packages', state.serviceProvider)] - ), - false - ); -}); - -test('random library file starting with . changed, fs is case sensitive', () => { - const code = ` -// global options -// @ignoreCase: false - `; - const state = parseAndGetTestState(code, '/projectRoot').state; - - assert.strictEqual( - state.workspace.service.test_shouldHandleLibraryFileWatchChanges( - Uri.file('/lib/.test.py', state.serviceProvider), - [Uri.file('/LIB', state.serviceProvider), Uri.file('/lib/site-packages', state.serviceProvider)] - ), - true - ); -}); - -test('random library file under a folder starting with . changed', () => { - const state = parseAndGetTestState('', '/projectRoot').state; - - assert.strictEqual( - state.workspace.service.test_shouldHandleLibraryFileWatchChanges( - Uri.file('/site-packages/.testFolder/test.py', state.serviceProvider), - [Uri.file('/site-packages', state.serviceProvider)] - ), - false - ); -}); - -test('basic file change', () => { - const code = ` -// @filename: test.py -//// [|/*marker*/|] - `; - - testSourceFileWatchChange(code); -}); - -test('non python file', () => { - const code = ` -// @filename: test.pyc -//// [|/*marker*/|] - `; - - testSourceFileWatchChange(code, /* expected */ false); -}); - -test('temp file', () => { - const code = ` -// @filename: test.py.12345678901234567890123456789012.py -//// [|/*marker*/|] - `; - - testSourceFileWatchChange(code, /* expected */ false); -}); - -test('excluded file', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "exclude": ["**/excluded.py"] -//// } - -// @filename: included.py -//// # empty - -// @filename: excluded.py -//// [|/*marker*/|] - `; - - testSourceFileWatchChange(code, /* expected */ false); -}); - -test('excluded but still part of program', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "exclude": ["**/excluded.py"] -//// } - -// @filename: included.py -//// from . import excluded - -// @filename: excluded.py -//// [|/*marker*/|] - `; - - const state = parseAndGetTestState(code, '/projectRoot').state; - const marker = state.getMarkerByName('marker'); - - while (state.workspace.service.test_program.analyze()); - - assert.strictEqual( - state.workspace.service.test_shouldHandleSourceFileWatchChanges(marker.fileUri, /* isFile */ true), - true - ); -}); - -test('py.typed marker file', () => { - const code = ` -// @filename: myPkg/__init__.py -//// # empty - -// @filename: myPkg/py.typed -//// [|/*marker*/|] - `; - - testSourceFileWatchChange(code, /* expected */ true); -}); - -test('py.typed marker file outside workspace semantics', () => { - const code = ` -// @filename: random/py.typed -//// [|/*marker*/|] - `; - - testSourceFileWatchChange(code, /* expected */ false); -}); - -test('py.typed marker file add causes invalidation', () => { - const code = ` -// @filename: myPkg/__init__.py -//// # empty - `; - - const state = parseAndGetTestState(code, '/projectRoot').state; - const pyTypedUri = Uri.file('/projectRoot/myPkg/py.typed', state.serviceProvider); - - // Setup the file watcher for the project. - const cmdOptions = new CommandLineOptions(state.workspace.rootUri, false); - cmdOptions.languageServerSettings.watchForSourceChanges = true; - state.workspace.service.setOptions(cmdOptions); - - let invalidatedReason: InvalidatedReason | undefined; - state.workspace.service.test_setOnInvalidatedCallback((reason) => { - invalidatedReason = reason; - }); - - state.testFS.writeFileSync(pyTypedUri, 'marker'); - state.testFS.fireFileWatcherEvent(pyTypedUri.toString(), 'add'); - - assert.strictEqual(invalidatedReason, InvalidatedReason.SourceWatcherChanged); -}); - -test('py.typed marker file change causes invalidation', () => { - const code = ` -// @filename: myPkg/__init__.py -//// # empty - -// @filename: myPkg/py.typed -//// marker - `; - - const state = parseAndGetTestState(code, '/projectRoot').state; - const pyTypedUri = Uri.file('/projectRoot/myPkg/py.typed', state.serviceProvider); - - // Setup the file watcher for the project. - const cmdOptions = new CommandLineOptions(state.workspace.rootUri, false); - cmdOptions.languageServerSettings.watchForSourceChanges = true; - state.workspace.service.setOptions(cmdOptions); - - let invalidatedReason: InvalidatedReason | undefined; - state.workspace.service.test_setOnInvalidatedCallback((reason) => { - invalidatedReason = reason; - }); - - state.testFS.writeFileSync(pyTypedUri, 'changed'); - state.testFS.fireFileWatcherEvent(pyTypedUri.toString(), 'change'); - - assert.strictEqual(invalidatedReason, InvalidatedReason.SourceWatcherChanged); -}); - -test('py.typed marker file delete causes invalidation', () => { - const code = ` -// @filename: myPkg/__init__.py -//// # empty - -// @filename: myPkg/py.typed -//// marker - `; - - const state = parseAndGetTestState(code, '/projectRoot').state; - const pyTypedUri = Uri.file('/projectRoot/myPkg/py.typed', state.serviceProvider); - - // Setup the file watcher for the project. - const cmdOptions = new CommandLineOptions(state.workspace.rootUri, false); - cmdOptions.languageServerSettings.watchForSourceChanges = true; - state.workspace.service.setOptions(cmdOptions); - - let invalidatedReason: InvalidatedReason | undefined; - state.workspace.service.test_setOnInvalidatedCallback((reason) => { - invalidatedReason = reason; - }); - - // The watcher reports only 'add' and 'change'. Deletions are inferred when the path can't be stat'ed. - state.testFS.unlinkSync(pyTypedUri); - state.testFS.fireFileWatcherEvent(pyTypedUri.toString(), 'change'); - - assert.strictEqual(invalidatedReason, InvalidatedReason.SourceWatcherChanged); -}); - -test('random folder changed', () => { - const code = ` -// @filename: notUsed.py -//// # empty - `; - - const state = parseAndGetTestState(code, '/projectRoot').state; - - assert.strictEqual( - state.workspace.service.test_shouldHandleSourceFileWatchChanges( - Uri.file('/randomFolder', state.serviceProvider), - /* isFile */ false - ), - false - ); -}); - -test('excluded folder changed', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "exclude": ["**/.*"] -//// } - -// @filename: .excluded/notUsed.py -//// [|/*marker*/|] - `; - - testSourceFileWatchChange(code, /* expected */ false, /* isFile */ false); -}); - -test('file under excluded folder changed', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "exclude": ["**/.*"] -//// } - -// @filename: included.py -//// # empty - -// @filename: .excluded/notUsed.py -//// [|/*marker*/|] - `; - - testSourceFileWatchChange(code, /* expected */ false); -}); - -test('folder under excluded folder changed', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "exclude": ["**/.*"] -//// } - -// @filename: .excluded/nested/notUsed.py -//// [|/*marker*/|] - `; - - testSourceFileWatchChange(code, /* expected */ false, /* isFile */ false); -}); - -test('folder that contains no file has changed', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "exclude": ["**/excluded.py"] -//// } - -// @filename: included.py -//// # empty - -// @filename: lib/excluded.py -//// [|/*marker*/|] - `; - - testSourceFileWatchChange(code, /* expected */ false, /* isFile */ false); -}); - -test('folder that contains a file has changed', () => { - const code = ` -// @filename: lib/included.py -//// [|/*marker*/|] - `; - - testSourceFileWatchChange(code, /* expected */ true, /* isFile */ false); -}); - -test('folder that contains no file but whose parent has __init__ has changed', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "exclude": ["**/excluded.py"] -//// } - -// @filename: lib/__init__.py -//// # empty - -// @filename: lib/nested/excluded.py -//// [|/*marker*/|] - `; - - testSourceFileWatchChange(code, /* expected */ true, /* isFile */ false); -}); - -test('library file watching for extra path under workspace', () => { - const watchers = getRegisteredLibraryFileWatchers('/src', ['extraPath'], ['extraPath/**']); - assert(watchers.some((w) => w.paths.some((p) => p.equals(UriEx.file('/src/extraPath'))))); -}); - -test('user file watching as extra path under workspace', () => { - // Sometimes, this trick is used to make sub-modules to top-level modules. - const watchers = getRegisteredLibraryFileWatchers('/src', ['extraPath']); - - // This shouldn't be recognized as library file. - assert(!watchers.some((w) => w.paths.some((p) => p.equals(UriEx.file('/src/extraPath'))))); -}); - -test('library file watching another workspace root using extra path', () => { - // The extra path for a different workspace root will be initially added as a relative path, - // but when it reaches the service layer, it will be normalized to an absolute path. - // That's why it is used as an absolute path here. - const watchers = getRegisteredLibraryFileWatchers('/root1', ['/root2']); - assert(watchers.some((w) => w.paths.some((p) => p.equals(UriEx.file('/root2'))))); -}); - -test('program containsSourceFileIn', () => { - const code = ` -// @ignoreCase: true - -// @filename: myLib/__init__.py -//// # empty - `; - - const state = parseAndGetTestState(code, '/projectRoot').state; - assert(state.workspace.service.test_program.containsSourceFileIn(state.activeFile.fileUri)); -}); - -test('service runEditMode', () => { - const code = ` -// @filename: open.py -//// /*open*/ - -// @filename: closed.py -//// /*closed*/ - `; - - const state = parseAndGetTestState(code, '/projectRoot').state; - const open = state.getMarkerByName('open'); - const closed = state.getMarkerByName('closed'); - const openUri = open.fileUri; - const closedUri = closed.fileUri; - - const newFileUri = Uri.file(combinePaths(getDirectoryPath(open.fileName), 'interimFile.py'), state.serviceProvider); - state.testFS.writeFileSync(newFileUri, '# empty', 'utf8'); - - const options = { - ipythonMode: IPythonMode.None, - chainedFileUri: newFileUri, - }; - - // try run edit mode - verifyRunEditMode('# first'); - - // try run again to make sure things are cleared up correctly - verifyRunEditMode('# second'); - - function verifyRunEditMode(value: string) { - state.workspace.service.runEditMode((p) => { - p.addInterimFile(newFileUri); - p.setFileOpened(openUri, 0, value, options); - p.setFileOpened(closedUri, 0, value, options); - - const interim = p.getSourceFileInfo(newFileUri); - assert(interim); - - const openFile = p.getSourceFileInfo(openUri); - assert(openFile); - assert(openFile.isOpenByClient); - assert.strictEqual(value, openFile.contents); - - const closedFile = p.getSourceFileInfo(closedUri); - assert(closedFile); - assert(closedFile.isOpenByClient); - assert.strictEqual(value, closedFile.contents); - }, CancellationToken.None); - - const interim = state.workspace.service.test_program.getSourceFileInfo(newFileUri); - assert(!interim); - - const openFile = state.workspace.service.test_program.getSourceFileInfo(openUri); - assert(openFile); - assert(openFile.isOpenByClient); - - assert.strictEqual('', openFile.contents?.trim()); - - const closedFile = state.workspace.service.test_program.getSourceFileInfo(closedUri); - assert(closedFile); - assert(!closedFile.isOpenByClient); - - const content = closedFile.contents ?? ''; - assert.strictEqual('', content.trim()); - } -}); - -test('setFileOpened does not change tracked state for existing source files', () => { - const state = parseAndGetTestState('', '/projectRoot').state; - const program = state.workspace.service.test_program; - const uri = UriEx.file('/projectRoot/interim.py'); - - program.addInterimFile(uri); - - const sourceFileInfo = program.getSourceFileInfo(uri); - assert(sourceFileInfo); - assert.strictEqual(sourceFileInfo.isTracked, false); - - // Opening an existing untracked file does not change its tracked state. - // Tracking is determined at creation time. - program.setFileOpened(uri, 1, 'value = 1', { - ipythonMode: IPythonMode.None, - chainedFileUri: undefined, - }); - - assert.strictEqual(sourceFileInfo.isOpenByClient, true); - assert.strictEqual(sourceFileInfo.isTracked, false); -}); - -test('setTrackedFiles does not untrack virtual open files', () => { - const state = parseAndGetTestState('', '/projectRoot').state; - const program = state.workspace.service.test_program; - const uri = Uri.parse('vscode-copilot-chat-code-block://conversation/block1.py', state.serviceProvider); - - program.setFileOpened(uri, 1, 'value = 1', { - isVirtual: true, - ipythonMode: IPythonMode.None, - chainedFileUri: undefined, - }); - - program.setTrackedFiles([]); - - const sourceFileInfo = program.getSourceFileInfo(uri); - assert(sourceFileInfo); - assert.strictEqual(sourceFileInfo.isOpenByClient, true); - assert.strictEqual(sourceFileInfo.isVirtual, true); - assert.strictEqual(sourceFileInfo.isTracked, true); -}); - -test('setTrackedFiles does not preserve tracked state for non-virtual open files', () => { - const state = parseAndGetTestState('', '/projectRoot').state; - const program = state.workspace.service.test_program; - const uri = UriEx.file('/projectRoot/interim.py'); - - state.testFS.mkdirpSync('/projectRoot'); - state.testFS.writeFileSync(uri, 'value = 1'); - program.addTrackedFile(uri); - program.setFileOpened(uri, 1, 'value = 1', { - ipythonMode: IPythonMode.None, - chainedFileUri: undefined, - }); - - program.setTrackedFiles([]); - - const sourceFileInfo = program.getSourceFileInfo(uri); - assert(sourceFileInfo); - assert.strictEqual(sourceFileInfo.isOpenByClient, true); - assert.strictEqual(sourceFileInfo.isVirtual, false); - assert.strictEqual(sourceFileInfo.isTracked, false); -}); - -test('setFileClosed auto-untracks and removes virtual files from the source list', () => { - const state = parseAndGetTestState('', '/projectRoot').state; - const program = state.workspace.service.test_program; - const uri = Uri.parse('vscode-copilot-chat-code-block://conversation/block1.py', state.serviceProvider); - - program.setFileOpened(uri, 1, 'value = 1', { - isVirtual: true, - ipythonMode: IPythonMode.None, - chainedFileUri: undefined, - }); - - const sourceFileInfo = program.getSourceFileInfo(uri); - assert(sourceFileInfo); - assert.strictEqual(sourceFileInfo.isTracked, true); - assert.strictEqual(sourceFileInfo.isVirtual, true); - - program.setFileClosed(uri); - - // Virtual files are auto-untracked on close and removed from source list - assert.strictEqual(program.getSourceFileInfo(uri), undefined); -}); - -test('untitled files are treated as virtual and survive setTrackedFiles', () => { - const state = parseAndGetTestState('', '/projectRoot').state; - const program = state.workspace.service.test_program; - const uri = Uri.parse('untitled:Untitled-1.py', state.serviceProvider); - - program.setFileOpened(uri, 1, 'value = 1', { - isVirtual: uri.isUntitled(), - ipythonMode: IPythonMode.None, - chainedFileUri: undefined, - }); - - const sourceFileInfo = program.getSourceFileInfo(uri); - assert(sourceFileInfo); - assert.strictEqual(sourceFileInfo.isVirtual, true); - assert.strictEqual(sourceFileInfo.isTracked, true); - - // Untitled files should survive tracked-file refresh since they - // are in-memory-only and don't participate in disk enumeration. - program.setTrackedFiles([]); - - assert.strictEqual(sourceFileInfo.isTracked, true); -}); - -test('file changes cause semantic update', () => { - const code = ` -// @filename: open.py -//// import closed -//// /*open*/ - -// @filename: closed.py -//// /*closed*/ - `; - - const state = parseAndGetTestState(code, '/projectRoot').state; - const open = state.getMarkerByName('open'); - const closed = state.getMarkerByName('closed'); - const openUri = open.fileUri; - const closedUri = closed.fileUri; - const openContents = state.testFS.readFileSync(openUri, 'utf-8'); - const options = { - ipythonMode: IPythonMode.None, - chainedFileUri: undefined, - }; - const cmdOptions = new CommandLineOptions(state.workspace.rootUri, false); - cmdOptions.languageServerSettings.watchForSourceChanges = true; - state.workspace.service.setOptions(cmdOptions); - - // Changing the closed file should update the semantic version of the open file as it is - // imported by it. - const p = state.workspace.service.test_program; - p.setFileOpened(openUri, 0, openContents, options); - // Do a parse so that imports are processed but not a full analysis as that would load - // the closed file into memory. - p.getParseResults(openUri); - const openFile = p.getSourceFileInfo(openUri); - assert(openFile); - assert(openFile.isOpenByClient); - assert.strictEqual(openContents, openFile.contents); - assert.strictEqual(openFile.imports.length, 3); - const oldSemanticVersion = openFile.semanticVersion; - state.testFS.writeFileSync(closedUri, 'print("changed")'); - state.testFS.fireFileWatcherEvent(closedUri.toString(), 'change'); - assert.strictEqual(openFile.semanticVersion, oldSemanticVersion + 1); -}); - -function testSourceFileWatchChange(code: string, expected = true, isFile = true) { - const state = parseAndGetTestState(code, '/projectRoot').state; - const marker = state.getMarkerByName('marker'); - const path = isFile ? marker.fileName : getDirectoryPath(marker.fileName); - - assert.strictEqual( - state.workspace.service.test_shouldHandleSourceFileWatchChanges(Uri.file(path, state.serviceProvider), isFile), - expected - ); -} - -function getRegisteredLibraryFileWatchers(root: string, extraPaths: string[], excludes: string[] = []) { - root = normalizeSlashes(root); - - const data = parseTestData(root, '', ''); - const state = new TestState(root, data); - - const options = new CommandLineOptions(state.workspace.rootUri, false); - options.languageServerSettings.watchForLibraryChanges = true; - options.configSettings.extraPaths = extraPaths; - options.configSettings.excludeFileSpecs = excludes; - - state.workspace.service.setOptions(options); - - return state.testFS.fileWatchers; -} diff --git a/packages/vipeye-internal/src/tests/signatureHelp.test.ts b/packages/vipeye-internal/src/tests/signatureHelp.test.ts deleted file mode 100644 index 2a45646a..00000000 --- a/packages/vipeye-internal/src/tests/signatureHelp.test.ts +++ /dev/null @@ -1,607 +0,0 @@ -/* - * signatureHelp.test.ts - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * - * Unit tests for signature help. - */ - -import assert from 'assert'; -import { CancellationToken, MarkupKind, SignatureHelp } from 'vscode-languageserver'; -import { SignatureHelpProvider } from '../languageService/signatureHelpProvider'; -import { parseAndGetTestState, TestState } from './harness/fourslash/testState'; -import { ViperDocStringService } from '../common/docStringService'; - -test('invalid position in format string segment', () => { - const code = ` -// @filename: test.py -//// f'{"(".capit[|/*marker*/|]alize()}' - `; - - checkSignatureHelp(code, false); -}); - -test('valid position in format string segment', () => { - const code = ` -// @filename: test.py -//// f'{"(".capitalize([|/*marker*/|])}' - `; - - checkSignatureHelp(code, true); -}); - -test('valid position in the second format string segment', () => { - const code = ` -// @filename: test.py -//// f'{print("hello")} {"(".capitalize([|/*marker*/|])}' - `; - - checkSignatureHelp(code, true); -}); - -test('invalid position in the second format string segment', () => { - const code = ` -// @filename: test.py -//// f'{print("hello")} {"(".capitalize [|/*marker*/|] ()}' - `; - - checkSignatureHelp(code, false); -}); - -test('nested call in format string segment', () => { - const code = ` -// @filename: test.py -//// def foo(): -//// pass -//// -//// f'{"(".capitalize(foo([|/*marker*/|]))}' - `; - - checkSignatureHelp(code, true); -}); - -test('signature help shows source default values for stub ellipsis defaults', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import mylib -//// -//// mylib.f([|/*marker*/|] - -// @filename: mylib.pyi -//// def f(a: int, b: str = ...) -> None: ... - -// @filename: mylib.py -//// def f(a: int, b: int = 3) -> None: -//// ... - `; - - const state = parseAndGetTestState(code).state; - const actual = getSignatureHelpForMarker(state, 'marker'); - - assert(actual, 'Expected signature help result'); - assert.strictEqual(actual.signatures.length, 1, 'Expected one signature'); - assert.strictEqual(actual.signatures[0].label, '(a: int, b: str = 3) -> None'); -}); - -test('signature help substitutes multiple stub ellipsis defaults from source', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import mylib -//// -//// mylib.f([|/*marker*/|] - -// @filename: mylib.pyi -//// def f(a: int = ..., b: str = ...) -> None: ... - -// @filename: mylib.py -//// def f(a: int = 3, b: str = "hello") -> None: -//// ... - `; - - const state = parseAndGetTestState(code).state; - const actual = getSignatureHelpForMarker(state, 'marker'); - - assert(actual, 'Expected signature help result'); - assert.strictEqual(actual.signatures.length, 1, 'Expected one signature'); - - assert.strictEqual(actual.signatures[0].label, '(a: int = 3, b: str = "hello") -> None'); -}); - -test('signature help shows concrete default values when provided by stub', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import mylib -//// -//// mylib.f([|/*marker*/|] - -// @filename: mylib.pyi -//// def f(a: int = 3, b: str = "hello") -> None: ... - `; - - const state = parseAndGetTestState(code).state; - const actual = getSignatureHelpForMarker(state, 'marker'); - - assert(actual, 'Expected signature help result'); - assert.strictEqual(actual.signatures.length, 1, 'Expected one signature'); - - assert.strictEqual(actual.signatures[0].label, '(a: int = 3, b: str = "hello") -> None'); -}); - -test('signature help substitutes stub ellipsis defaults for all overloads', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import mylib -//// -//// mylib.f(1, [|/*marker*/|]) - -// @filename: mylib.pyi -//// from typing import overload -//// -//// @overload -//// def f(a: int = ..., b: str = ...) -> None: ... -//// -//// @overload -//// def f(a: str = ..., b: str = ...) -> None: ... -//// -//// def f(*args, **kwargs) -> None: ... - -// @filename: mylib.py -//// def f(a: int = 3, b: str = "hello") -> None: -//// ... - `; - - const state = parseAndGetTestState(code).state; - const actual = getSignatureHelpForMarker(state, 'marker'); - - assert(actual, 'Expected signature help result'); - - // Python runtime has a single implementation, so source defaults apply to all overloads. - assert.strictEqual(actual.activeSignature, 0); - assert.strictEqual(actual.signatures.length, 2); - assert.strictEqual(actual.signatures[0].label, '(a: int = 3, b: str = "hello") -> None'); - assert.strictEqual(actual.signatures[1].label, '(a: str = 3, b: str = "hello") -> None'); -}); - -test('signature help does not substitute when stub default is not ellipsis', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import mylib -//// -//// mylib.f([|/*marker*/|] - -// @filename: mylib.pyi -//// def f(a: int, b: str = 1) -> None: ... - -// @filename: mylib.py -//// def f(a: int, b: int = 3) -> None: -//// ... - `; - - const state = parseAndGetTestState(code).state; - const actual = getSignatureHelpForMarker(state, 'marker'); - - assert(actual, 'Expected signature help result'); - assert.strictEqual(actual.signatures.length, 1, 'Expected one signature'); - - assert.strictEqual(actual.signatures[0].label, '(a: int, b: str = 1) -> None'); -}); - -test('signature help does not substitute when source implementation is missing', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import mylib -//// -//// mylib.f([|/*marker*/|] - -// @filename: mylib.pyi -//// def f(a: int, b: str = ...) -> None: ... - `; - - const state = parseAndGetTestState(code).state; - const actual = getSignatureHelpForMarker(state, 'marker'); - - assert(actual, 'Expected signature help result'); - assert.strictEqual(actual.signatures.length, 1, 'Expected one signature'); - assert.strictEqual(actual.signatures[0].label, '(a: int, b: str = ...) -> None'); -}); - -test('signature help does not substitute unsafe long defaults for stub ellipsis defaults', () => { - const longNumber = '9'.repeat(150); - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import mylib -//// -//// mylib.f([|/*marker*/|] - -// @filename: mylib.pyi -//// def f(a: int, b: str = ...) -> None: ... - -// @filename: mylib.py -//// def f(a: int, b: int = ${longNumber}) -> None: -//// ... - `; - - const state = parseAndGetTestState(code).state; - const actual = getSignatureHelpForMarker(state, 'marker'); - - assert(actual, 'Expected signature help result'); - assert.strictEqual(actual.signatures.length, 1, 'Expected one signature'); - - assert.strictEqual(actual.signatures[0].label, '(a: int, b: str = ...) -> None'); -}); - -test('signature help does not substitute unsafe multiline defaults for stub ellipsis defaults', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import mylib -//// -//// mylib.f([|/*marker*/|] - -// @filename: mylib.pyi -//// def f(a: int, b: str = ...) -> None: ... - -// @filename: mylib.py -//// def f(a: int, b: int = """hello -//// world""") -> None: -//// ... - `; - - const state = parseAndGetTestState(code).state; - const actual = getSignatureHelpForMarker(state, 'marker'); - - assert(actual, 'Expected signature help result'); - assert.strictEqual(actual.signatures.length, 1, 'Expected one signature'); - - assert.strictEqual(actual.signatures[0].label, '(a: int, b: str = ...) -> None'); -}); - -test('signature help overloads: mixed ellipsis and concrete defaults', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import mylib -//// -//// mylib.f([|/*marker*/|]) - -// @filename: mylib.pyi -//// from typing import overload -//// -//// @overload -//// def f(a: int, b: str = ...) -> None: ... -//// -//// @overload -//// def f(a: int, b: str = "default") -> None: ... -//// -//// def f(*args, **kwargs) -> None: ... - -// @filename: mylib.py -//// def f(a: int, b: str = "hello") -> None: -//// ... - `; - - const state = parseAndGetTestState(code).state; - const actual = getSignatureHelpForMarker(state, 'marker'); - - assert(actual, 'Expected signature help result'); - assert.strictEqual(actual.signatures.length, 2); - // First overload: ellipsis substituted from source - assert.strictEqual(actual.signatures[0].label, '(a: int, b: str = "hello") -> None'); - // Second overload: concrete default kept as-is (not ellipsis, no substitution) - assert.strictEqual(actual.signatures[1].label, '(a: int, b: str = "default") -> None'); -}); - -test('signature help overloads: overload with no defaults stays unchanged', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import mylib -//// -//// mylib.f([|/*marker*/|]) - -// @filename: mylib.pyi -//// from typing import overload -//// -//// @overload -//// def f(a: int) -> None: ... -//// -//// @overload -//// def f(a: int, b: str = ...) -> None: ... -//// -//// def f(*args, **kwargs) -> None: ... - -// @filename: mylib.py -//// def f(a: int, b: str = "hello") -> None: -//// ... - `; - - const state = parseAndGetTestState(code).state; - const actual = getSignatureHelpForMarker(state, 'marker'); - - assert(actual, 'Expected signature help result'); - assert.strictEqual(actual.signatures.length, 2); - // First overload: no default parameter, stays unchanged - assert.strictEqual(actual.signatures[0].label, '(a: int) -> None'); - // Second overload: ellipsis substituted from source - assert.strictEqual(actual.signatures[1].label, '(a: int, b: str = "hello") -> None'); -}); - -test('signature help overloads: param name not in source keeps ellipsis', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import mylib -//// -//// mylib.f([|/*marker*/|]) - -// @filename: mylib.pyi -//// from typing import overload -//// -//// @overload -//// def f(a: int, b: str = ...) -> None: ... -//// -//// @overload -//// def f(a: int, x: str = ...) -> None: ... -//// -//// def f(*args, **kwargs) -> None: ... - -// @filename: mylib.py -//// def f(a: int, b: str = "hello") -> None: -//// ... - `; - - const state = parseAndGetTestState(code).state; - const actual = getSignatureHelpForMarker(state, 'marker'); - - assert(actual, 'Expected signature help result'); - assert.strictEqual(actual.signatures.length, 2); - // First overload: 'b' found in source, substituted - assert.strictEqual(actual.signatures[0].label, '(a: int, b: str = "hello") -> None'); - // Second overload: 'x' not in source, keeps ellipsis - assert.strictEqual(actual.signatures[1].label, '(a: int, x: str = ...) -> None'); -}); - -test('signature help overloads: source uses *args/**kwargs, no substitution', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import mylib -//// -//// mylib.f([|/*marker*/|]) - -// @filename: mylib.pyi -//// from typing import overload -//// -//// @overload -//// def f(a: int = ...) -> None: ... -//// -//// @overload -//// def f(a: str = ...) -> None: ... -//// -//// def f(*args, **kwargs) -> None: ... - -// @filename: mylib.py -//// def f(*args, **kwargs) -> None: -//// ... - `; - - const state = parseAndGetTestState(code).state; - const actual = getSignatureHelpForMarker(state, 'marker'); - - assert(actual, 'Expected signature help result'); - assert.strictEqual(actual.signatures.length, 2); - // Source has no named params with defaults, so ellipsis stays - assert.strictEqual(actual.signatures[0].label, '(a: int = ...) -> None'); - assert.strictEqual(actual.signatures[1].label, '(a: str = ...) -> None'); -}); - -test('signature help overloads: different param counts, extra params keep ellipsis', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: test.py -//// import mylib -//// -//// mylib.f([|/*marker*/|]) - -// @filename: mylib.pyi -//// from typing import overload -//// -//// @overload -//// def f(a: int = ..., b: str = ..., c: float = ...) -> None: ... -//// -//// @overload -//// def f(a: int = ...) -> None: ... -//// -//// def f(*args, **kwargs) -> None: ... - -// @filename: mylib.py -//// def f(a: int = 1, b: str = "hi") -> None: -//// ... - `; - - const state = parseAndGetTestState(code).state; - const actual = getSignatureHelpForMarker(state, 'marker'); - - assert(actual, 'Expected signature help result'); - assert.strictEqual(actual.signatures.length, 2); - // First overload: a and b substituted, c not in source keeps ellipsis - assert.strictEqual(actual.signatures[0].label, '(a: int = 1, b: str = "hi", c: float = ...) -> None'); - // Second overload: a substituted - assert.strictEqual(actual.signatures[1].label, '(a: int = 1) -> None'); -}); - -test('within arguments in format string segment', () => { - const code = ` -// @filename: test.py -//// def foo(): -//// pass -//// -//// f'{"(".capitalize(fo[|/*marker*/|]o())}' - `; - - checkSignatureHelp(code, true); -}); - -// Wrong signature hint with an unpacked parameterized TypedDict as a variadic keyword parameter. -// When a function declares `**kwargs: Unpack[GenericTypedDict[int]]`, signature help expands the -// TypedDict entries but must substitute the type argument: the `t` entry should print as `int`, -// not the unspecialized TypeVar `T@TD`. -test('signature help substitutes type argument for unpacked generic TypedDict kwargs', () => { - const code = ` -// @filename: test.py -//// from typing import Generic, TypedDict, TypeVar, Unpack -//// -//// T = TypeVar('T') -//// -//// class TD(TypedDict, Generic[T]): -//// t: T -//// -//// def func(**kwargs: Unpack[TD[int]]) -> None: -//// pass -//// -//// func([|/*marker*/|]) - `; - - const state = parseAndGetTestState(code).state; - const actual = getSignatureHelpForMarker(state, 'marker'); - - assert(actual, 'Expected signature help for unpacked generic TypedDict kwargs'); - assert.strictEqual(actual.signatures.length, 1, 'Expected one signature'); - assert.strictEqual(actual.signatures[0].label, '(*, t: int) -> None'); -}); - -// Additional coverage for the unpacked generic TypedDict kwargs case. -// The fix specializes TypedDict entries against the (possibly generic) TypedDict instance at the -// param value type, the default-param value type (only exercised by non-required entries), and -// nested generic value types. These variants guard the non-required (default-param) branch, -// per-entry specialization with a concrete sibling entry, and nested generic value types. -test('signature help substitutes type argument for unpacked generic TypedDict kwargs variants', () => { - const code = ` -// @filename: test.py -//// from typing import Generic, NotRequired, TypedDict, TypeVar, Unpack -//// -//// T = TypeVar('T') -//// -//// class TDNotRequired(TypedDict, Generic[T]): -//// t: NotRequired[T] -//// -//// class TDMixed(TypedDict, Generic[T]): -//// a: T -//// b: str -//// -//// class TDNested(TypedDict, Generic[T]): -//// t: list[T] -//// -//// def func_notrequired(**kwargs: Unpack[TDNotRequired[int]]) -> None: ... -//// def func_mixed(**kwargs: Unpack[TDMixed[int]]) -> None: ... -//// def func_nested(**kwargs: Unpack[TDNested[int]]) -> None: ... -//// -//// func_notrequired([|/*notRequired*/|]) -//// func_mixed([|/*mixed*/|]) -//// func_nested([|/*nested*/|]) - `; - - const state = parseAndGetTestState(code).state; - - // Non-required entries exercise the default-param specialization branch: the type argument must - // still be substituted (`t: int = ...`), not left as the TypeVar `t: T@... = ...`. - const notRequired = getSignatureHelpForMarker(state, 'notRequired'); - assert(notRequired, "Expected signature help at 'notRequired'"); - assert.strictEqual(notRequired.signatures.length, 1); - assert.strictEqual(notRequired.signatures[0].label, '(*, t: int = ...) -> None'); - - // Mixed entries: the generic entry is substituted while the concrete sibling is unchanged. - const mixed = getSignatureHelpForMarker(state, 'mixed'); - assert(mixed, "Expected signature help at 'mixed'"); - assert.strictEqual(mixed.signatures.length, 1); - assert.strictEqual(mixed.signatures[0].label, '(*, a: int, b: str) -> None'); - - // Nested generic value types must specialize recursively (`list[T]` -> `list[int]`). - const nested = getSignatureHelpForMarker(state, 'nested'); - assert(nested, "Expected signature help at 'nested'"); - assert.strictEqual(nested.signatures.length, 1); - assert.strictEqual(nested.signatures[0].label, '(*, t: list[int]) -> None'); -}); - -function checkSignatureHelp(code: string, expects: boolean) { - const state = parseAndGetTestState(code).state; - const actual = getSignatureHelpForMarker(state, 'marker'); - - assert.strictEqual(!!actual, expects); -} - -function getSignatureHelpForMarker(state: TestState, markerName: string): SignatureHelp | undefined { - const marker = state.getMarkerByName(markerName); - const position = state.getPosition(markerName); - - return new SignatureHelpProvider( - state.workspace.service.test_program, - marker.fileUri, - position, - MarkupKind.Markdown, - /*hasSignatureLabelOffsetCapability*/ true, - /*hasActiveParameterCapability*/ true, - /*context*/ undefined, - new ViperDocStringService(), - CancellationToken.None - ).getSignatureHelp(); -} diff --git a/packages/vipeye-internal/src/tests/sourceMapperUtils.test.ts b/packages/vipeye-internal/src/tests/sourceMapperUtils.test.ts deleted file mode 100644 index 1b08bd79..00000000 --- a/packages/vipeye-internal/src/tests/sourceMapperUtils.test.ts +++ /dev/null @@ -1,502 +0,0 @@ -/* - * sourceFile.test.ts - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * - * Unit tests for viper sourceMapperUtils module. - */ -import assert from 'assert'; -import { CancellationToken, CancellationTokenSource } from 'vscode-jsonrpc'; -import { MarkupKind } from 'vscode-languageserver'; - -import { AnalyzerService } from '../analyzer/service'; -import { - FunctionDeclaration, - VariableDeclaration, - isFunctionDeclaration, - isVariableDeclaration, -} from '../analyzer/declaration'; -import { findNodeByOffset } from '../analyzer/parseTreeUtils'; -import { buildImportTree as buildImportTreeImpl } from '../analyzer/sourceMapperUtils'; -import { TypeCategory } from '../analyzer/types'; -import { ConfigOptions } from '../common/configOptions'; -import { NullConsole } from '../common/console'; -import { combinePaths, getDirectoryPath, normalizeSlashes } from '../common/pathUtils'; -import { convertOffsetToPosition } from '../common/positionUtils'; -import { createServiceProvider } from '../common/serviceProviderExtensions'; -import { TextRange } from '../common/textRange'; -import { UriEx } from '../common/uri/uriUtils'; -import { HoverProvider } from '../languageService/hoverProvider'; -import { PartialStubService } from '../partialStubService'; -import { NameNode, ParseNodeType } from '../parser/parseNodes'; -import { ViperFileSystem } from '../viperFileSystem'; -import { parseTestData } from './harness/fourslash/fourSlashParser'; -import { TestAccessHost } from './harness/testAccessHost'; -import { getNodeAtMarker, parseAndGetTestState } from './harness/fourslash/testState'; -import { TestFileSystem } from './harness/vfs/filesystem'; - -function buildImportTree( - sourceFile: string, - targetFile: string, - importResolver: (f: string) => string[], - token: CancellationToken -): string[] { - return buildImportTreeImpl( - UriEx.file(sourceFile), - UriEx.file(targetFile), - (from) => { - const resolved = importResolver(from.getFilePath().slice(1)); - return resolved.map((f) => UriEx.file(f)); - }, - token - ).map((u) => u.getFilePath().slice(1)); -} - -describe('BuildImportTree', () => { - const tokenSource = new CancellationTokenSource(); - test('Simple', () => { - const results = buildImportTree( - 'A', - 'C', - (f) => { - switch (f) { - case 'C': - return ['B']; - case 'B': - return ['A']; - default: - break; - } - return []; - }, - tokenSource.token - ); - assert.deepEqual(results, ['C', 'B']); - }); - - test('Recursion', () => { - const results = buildImportTree( - 'A', - 'E', - (f) => { - switch (f) { - case 'E': - return ['D']; - case 'D': - return ['C', 'B']; - case 'C': - return ['D']; - case 'B': - return ['A']; - default: - break; - } - return []; - }, - tokenSource.token - ); - assert.deepEqual(results, ['E', 'D', 'B']); - }); - - test('Multiple Paths', () => { - const results = buildImportTree( - 'A', - 'G', - (f) => { - switch (f) { - case 'G': - return ['F', 'H', 'I']; - case 'F': - return ['D', 'E']; - case 'D': - return ['C', 'B']; - case 'C': - return ['E']; - case 'B': - return ['A']; - default: - break; - } - return []; - }, - tokenSource.token - ); - assert.deepEqual(results, ['G', 'F', 'D', 'B']); - }); - - test('No paths', () => { - const results = buildImportTree( - 'A', - 'G', - (f) => { - switch (f) { - case 'G': - return ['F', 'H', 'I']; - case 'F': - return ['D', 'E']; - case 'D': - return ['C', 'B']; - case 'C': - return ['E']; - default: - break; - } - return []; - }, - tokenSource.token - ); - assert.deepEqual(results, ['G']); - }); - - function genArray(start: number, end: number): string[] { - return Array(end - start) - .fill(0) - .map(() => String.fromCharCode(start++)); - } - - test('Too deep', () => { - const results = buildImportTree( - 'Z', - 'A', - (f) => { - const start = f.charCodeAt(0); - const end = 'Y'.charCodeAt(0); - return genArray(start, end); - }, - tokenSource.token - ); - assert.deepEqual(results, ['A']); - }); - - test('Canceled', () => { - const canceled = new CancellationTokenSource(); - canceled.cancel(); - const results = buildImportTree( - 'A', - 'E', - (f) => { - switch (f) { - case 'E': - return ['D']; - case 'D': - return ['C', 'B']; - case 'C': - return ['D']; - case 'B': - return ['A']; - default: - break; - } - return []; - }, - canceled.token - ); - assert.deepEqual(results, ['E']); - }); -}); - -test('find type alias decl', () => { - const code = ` -// @filename: test.py -//// from typing import Mapping -//// [|/*decl*/M|] = Mapping -//// -//// def foo(/*marker*/m: M): pass - `; - - assertTypeAlias(code); -}); - -test('find type alias decl from inferred type', () => { - const code = ` -// @filename: test.py -//// from typing import Mapping -//// [|/*decl*/M|] = Mapping -//// -//// def foo(m: M): -//// return m - -// @filename: test1.py -//// from test import foo -//// a = { "hello": 10 } -//// -//// /*marker*/b = foo(a) - `; - - assertTypeAlias(code); -}); - -function assertTypeAlias(code: string) { - const state = parseAndGetTestState(code).state; - - const node = getNodeAtMarker(state, 'marker'); - assert(node.nodeType === ParseNodeType.Name); - - const type = state.program.evaluator!.getType(node); - assert(type?.category === TypeCategory.Class); - - assert.strictEqual(type.shared.name, 'Mapping'); - assert.strictEqual(type.props?.typeAliasInfo?.shared.name, 'M'); - assert.strictEqual(type.props?.typeAliasInfo.shared.moduleName, 'test'); - - const marker = state.getMarkerByName('marker'); - const markerUri = marker.fileUri; - const mapper = state.program.getSourceMapper( - markerUri, - CancellationToken.None, - /* mapCompiled */ false, - /* preferStubs */ true - ); - - const range = state.getRangeByMarkerName('decl')!; - const decls = mapper.findDeclarationsByType(markerUri, type, /* userTypeAlias */ true); - - const decl = decls.find((d) => isVariableDeclaration(d) && d.typeAliasName && d.typeAliasName.d.value === 'M') as - | VariableDeclaration - | undefined; - assert(decl); - - assert.deepEqual(TextRange.create(decl.node.start, decl.node.length), TextRange.fromBounds(range.pos, range.end)); -} - -test('find method declarations through alias factory assignment', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "useLibraryCodeForTypes": true -//// } - -// @filename: _collections_abc.py -// @library: true -//// class MutableMapping: -//// def update(self, value=None): -//// '''Do the thing. -//// -//// Updates the internal state.''' -//// pass - -// @filename: _typing.pyi -// @library: true -//// from typing import overload -//// class MutableMapping: -//// @overload -//// def update(self) -> None: ... -//// @overload -//// def update(self, value: int) -> None: ... - -// @filename: _typing.py -// @library: true -//// from _collections_abc import MutableMapping as _MutableMapping -//// -//// def _alias(value, _parameter_count): -//// return value -//// -//// MutableMapping = _alias(_MutableMapping, 2) - -// @filename: _builtins.pyi -// @library: true -//// from _typing import MutableMapping -//// class MyDict(MutableMapping): -//// pass - -// @filename: test.py -//// from _builtins import MyDict -//// d = MyDict() -//// d./*marker*/update() - `; - - const state = parseAndGetTestState(code).state; - const node = getNodeAtMarker(state, 'marker') as NameNode; - - assert.strictEqual(node.nodeType, ParseNodeType.Name); - - const decls = state.program.evaluator!.getDeclInfoForNameNode(node)?.decls; - assert(decls && decls.length > 0, 'Expected declarations for update'); - - const stubDecls = decls.filter((decl): decl is FunctionDeclaration => isFunctionDeclaration(decl)); - assert(stubDecls.length > 0, 'Expected function declarations for update'); - - const marker = state.getMarkerByName('marker'); - const mapper = state.program.getSourceMapper( - marker.fileUri, - CancellationToken.None, - /* mapCompiled */ false, - /* preferStubs */ false - ); - - const implDecls = stubDecls.flatMap((decl) => mapper.findFunctionDeclarations(decl)); - - assert.deepStrictEqual(getUniqueDeclPaths(implDecls), ['/lib/site-packages/_collections_abc.py']); -}); - -test('find method declarations through typeshed stdlib alias chain', () => { - const stdlibSourceRoot = '/python-stdlib'; - const { data, service } = createAnalyzerServiceFromCode(getTypeshedStdlibAliasChainCode(), [stdlibSourceRoot]); - while (service.test_program.analyze()) { - // Continue until analysis completes. - } - - const marker = data.markerPositions.get('marker'); - assert(marker, 'Expected marker for update reference'); - - const sourceFile = service.test_program.getBoundSourceFile(marker.fileUri); - assert(sourceFile, 'Expected source file for marker file'); - - const parseResults = sourceFile.getParseResults(); - assert(parseResults, 'Expected parse results for marker file'); - - const node = findNodeByOffset(parseResults.parserOutput.parseTree, marker.position) as NameNode; - assert.strictEqual(node.nodeType, ParseNodeType.Name); - - const decls = service.test_program.evaluator!.getDeclInfoForNameNode(node)?.decls; - assert(decls && decls.length > 0, 'Expected declarations for update'); - - const stubDecls = decls.filter((decl): decl is FunctionDeclaration => isFunctionDeclaration(decl)); - assert(stubDecls.length > 0, 'Expected function declarations for update'); - - const mapper = service.test_program.getSourceMapper( - marker.fileUri, - CancellationToken.None, - /* mapCompiled */ false, - /* preferStubs */ false - ); - - const implDecls = stubDecls.flatMap((decl) => mapper.findFunctionDeclarations(decl)); - - assert.deepStrictEqual(getUniqueDeclPaths(implDecls), ['/python-stdlib/_collections_abc.py']); -}); - -test('hover shows inherited stdlib docstring through configured venv', () => { - const stdlibSourceRoot = '/python-stdlib'; - const venvPath = '/venvs'; - const venvName = 'test'; - const venvRoot = `${venvPath}/${venvName}`; - const venvSitePackages = `${venvRoot}/lib/site-packages`; - - const { data, service } = createAnalyzerServiceFromCode( - getTypeshedStdlibAliasChainCode(), - [stdlibSourceRoot, venvSitePackages], - { hostPrefix: venvRoot, venvPath, venv: venvName } - ); - while (service.test_program.analyze()) { - // Continue until analysis completes. - } - - const marker = data.markerPositions.get('marker'); - assert(marker, 'Expected marker for hover target'); - - const sourceFile = service.test_program.getBoundSourceFile(marker.fileUri); - assert(sourceFile, 'Expected source file for hover target'); - - const parseResults = sourceFile.getParseResults(); - assert(parseResults, 'Expected parse results for hover target'); - - const position = convertOffsetToPosition(marker.position, parseResults.tokenizerOutput.lines); - const hover = new HoverProvider( - service.test_program, - marker.fileUri, - position, - MarkupKind.Markdown, - CancellationToken.None - ).getHover(); - - assert(hover, 'Expected hover result for update'); - assert(!Array.isArray(hover.contents) && typeof hover.contents !== 'string', 'Expected structured hover contents'); - - const contents = hover.contents as { kind: MarkupKind; value: string }; - - assert.strictEqual(contents.kind, MarkupKind.Markdown); - const expectedHoverMarkdown = - '```python\n(method) def update(value: Unknown = None) -> Unknown\n```\n---\nDo the thing.\n\nUpdates the internal state.'; - assert.strictEqual(contents.value, expectedHoverMarkdown); -}); - -function getUniqueDeclPaths(decls: FunctionDeclaration[]) { - return Array.from(new Set(decls.map((decl) => normalizeSlashes(decl.uri.getFilePath(), '/')))); -} - -function getTypeshedStdlibAliasChainCode() { - return ` -// @filename: /typeshed-fallback/stdlib/typing.pyi -//// class MutableMapping: -//// def update(self, value=None) -> None: ... - -// @filename: /python-stdlib/_collections_abc.py -//// class MutableMapping: -//// def update(self, value=None): -//// '''Do the thing. -//// -//// Updates the internal state.''' -//// pass - -// @filename: /python-stdlib/typing.py -//// from _collections_abc import MutableMapping as _MutableMapping -//// -//// def _alias(value, _parameter_count): -//// return value -//// -//// MutableMapping = _alias(_MutableMapping, 2) - -// @filename: /typeshed-fallback/stdlib/builtins.pyi -//// from typing import MutableMapping -//// class dict(MutableMapping): -//// pass - -// @filename: /venvs/test/lib/site-packages/sentinel.py -//// x = 1 - -// @filename: /src/test.py -//// d = dict() -//// d./*marker*/update() - `; -} - -function createAnalyzerServiceFromCode(code: string, pythonSearchPaths: string[], options?: CustomAnalyzerOptions) { - const data = parseTestData('/', code, ''); - const testFS = new TestFileSystem(/* ignoreCase */ false, { cwd: normalizeSlashes('/') }); - - for (const file of data.files) { - const filePath = normalizeSlashes(file.fileName); - testFS.mkdirpSync(getDirectoryPath(filePath)); - testFS.writeFileSync(UriEx.file(filePath), file.content, 'utf8'); - } - - const fs = new ViperFileSystem(testFS); - const serviceProvider = createServiceProvider(testFS, fs, new PartialStubService(fs)); - const configOptions = new ConfigOptions(UriEx.file('/')); - configOptions.internalTestMode = true; - configOptions.useLibraryCodeForTypes = true; - - if (options?.venvPath && options.venv) { - configOptions.venvPath = UriEx.file(options.venvPath); - configOptions.venv = options.venv; - configOptions.pythonPath = UriEx.file(combinePaths(options.venvPath, options.venv, 'python')); - } - - const service = new AnalyzerService('test service', serviceProvider, { - console: new NullConsole(), - hostFactory: () => - new TestAccessHost( - options?.hostPrefix ? UriEx.file(options.hostPrefix) : serviceProvider.fs().getModulePath(), - pythonSearchPaths.map((path) => UriEx.file(path)), - serviceProvider.fs() - ), - importResolverFactory: AnalyzerService.createImportResolver, - configOptions, - fileSystem: fs, - libraryReanalysisTimeProvider: () => 0, - shouldRunAnalysis: () => true, - }); - - for (const file of data.files) { - service.setFileOpened(file.fileUri, 1, file.content); - } - - return { data, service }; -} - -interface CustomAnalyzerOptions { - hostPrefix?: string; - venvPath?: string; - venv?: string; -} diff --git a/packages/vipeye-internal/src/tests/testState.test.ts b/packages/vipeye-internal/src/tests/testState.test.ts deleted file mode 100644 index e72de20b..00000000 --- a/packages/vipeye-internal/src/tests/testState.test.ts +++ /dev/null @@ -1,618 +0,0 @@ -/* - * testState.test.ts - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * - * Tests and show how to use TestState in unit test - */ - -import assert from 'assert'; - -import { combinePaths, getFileName, normalizeSlashes } from '../common/pathUtils'; -import { compareStringsCaseSensitive } from '../common/stringUtils'; -import { Uri } from '../common/uri/uri'; -import { Range } from './harness/fourslash/fourSlashTypes'; -import { runFourSlashTestContent } from './harness/fourslash/runner'; -import { parseAndGetTestState } from './harness/fourslash/testState'; -import * as factory from './harness/vfs/factory'; - -test('Create', () => { - const code = ` -// @filename: file1.py -////class A: -//// pass - `; - - const { data, state } = parseAndGetTestState(code); - assert(state.activeFile === data.files[0]); -}); - -test('Multiple files', () => { - const code = ` -// @filename: file1.py -////class A: -//// pass - -// @filename: file2.py -////class B: -//// pass - -// @filename: file3.py -////class C: -//// pass - `; - - const state = parseAndGetTestState(code, factory.srcFolder).state; - - assert.equal(state.cwd(), normalizeSlashes('/')); - assert( - state.fs.existsSync( - Uri.file(normalizeSlashes(combinePaths(factory.srcFolder, 'file1.py')), state.serviceProvider) - ) - ); -}); - -test('Configuration', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "include": [ -//// "src" -//// ], -//// -//// "exclude": [ -//// "**/node_modules", -//// "**/__pycache__", -//// "src/experimental", -//// "src/web/node_modules", -//// "src/typestubs" -//// ], -//// -//// "ignore": [ -//// "src/oldstuff" -//// ], -//// -//// "typingsPath": "src/typestubs", -//// "venvPath": "/home/foo/.venvs", -//// -//// "reportMissingImports": true, -//// "reportMissingTypeStubs": false, -//// -//// "pythonVersion": "3.6", -//// "pythonPlatform": "Linux", -//// -//// "executionEnvironments": [ -//// { -//// "root": "src/web", -//// "pythonVersion": "3.5", -//// "pythonPlatform": "Windows", -//// "extraPaths": [ -//// "src/service_libs" -//// ] -//// }, -//// { -//// "root": "src/sdk", -//// "pythonVersion": "3.0", -//// "extraPaths": [ -//// "src/backend" -//// ], -//// "venv": "venv_bar" -//// }, -//// { -//// "root": "src/tests", -//// "extraPaths": [ -//// "src/tests/e2e", -//// "src/sdk" -//// ] -//// }, -//// { -//// "root": "src" -//// } -//// ] -//// } - -// @filename: file1.py -////class A: -//// pass - `; - - const state = parseAndGetTestState(code, factory.srcFolder).state; - - assert.equal(state.cwd(), normalizeSlashes('/')); - assert( - state.fs.existsSync( - Uri.file(normalizeSlashes(combinePaths(factory.srcFolder, 'file1.py')), state.serviceProvider) - ) - ); - - assert.equal(state.configOptions.diagnosticRuleSet.reportMissingImports, 'error'); - assert.equal(state.configOptions.diagnosticRuleSet.reportMissingModuleSource, 'warning'); - assert.equal(state.configOptions.stubPath?.getFilePath(), normalizeSlashes('/src/typestubs')); -}); - -test('stubPath configuration', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "stubPath": "src/typestubs" -//// } - `; - - const state = parseAndGetTestState(code).state; - assert.equal(state.configOptions.stubPath?.getFilePath(), normalizeSlashes('/src/typestubs')); -}); - -test('viperconfig.json exists in VFS', () => { - // Regression test: viperconfig.json should be written to the virtual file system, - // not just parsed for config options. - const code = ` -// @filename: viperconfig.json -//// { -//// "stubPath": "my_stubs" -//// } - -// @filename: test.py -//// x = 1 - `; - - const state = parseAndGetTestState(code, factory.srcFolder).state; - - // The viperconfig.json file should exist in the VFS - const configUri = Uri.file( - normalizeSlashes(combinePaths(factory.srcFolder, 'viperconfig.json')), - state.serviceProvider - ); - assert(state.fs.existsSync(configUri), `viperconfig.json should exist in VFS at ${configUri.toString()}`); - - // The config should also be parsed correctly - assert.equal(state.configOptions.stubPath?.getFilePath(), normalizeSlashes('/my_stubs')); -}); - -test('Duplicated stubPath configuration', () => { - const code = ` -// @filename: viperconfig.json -//// { -//// "typingsPath": "src/typestubs1", -//// "stubPath": "src/typestubs2" -//// } - `; - - const state = parseAndGetTestState(code).state; - assert.equal(state.configOptions.stubPath?.getFilePath(), normalizeSlashes('/src/typestubs2')); -}); - -test('ProjectRoot', () => { - const code = ` -// global options -// @projectRoot: /root - -// @filename: /root/file1.py -////class A: -//// pass - `; - - const state = parseAndGetTestState(code).state; - - assert.equal(state.cwd(), normalizeSlashes('/root')); - assert(state.fs.existsSync(Uri.file(normalizeSlashes('/root/file1.py'), state.serviceProvider))); - - assert.equal(state.configOptions.projectRoot.getFilePath(), normalizeSlashes('/root')); -}); - -test('CustomTypeshedFolder', () => { - // use differnt physical folder as typeshed folder. this is different than - // typeshed folder settings in config json file since that points to a path - // in virtual file system. not physical one. this decides which physical folder - // those virtual folder will mount to. - const code = ` -// global options -// @typeshed: ${__dirname} - `; - - // mount the folder this file is in as typeshed folder and check whether - // in typeshed folder in virtual file system, this file exists. - const state = parseAndGetTestState(code).state; - assert(state.fs.existsSync(factory.typeshedFolder.combinePaths(getFileName(__filename)))); -}); - -test('IgnoreCase', () => { - const code = ` -// global options -// @ignoreCase: true - -// @filename: file1.py -////class A: -//// pass - `; - - const state = parseAndGetTestState(code, factory.srcFolder).state; - - assert( - state.fs.existsSync( - Uri.file(normalizeSlashes(combinePaths(factory.srcFolder, 'FILE1.py')), state.serviceProvider) - ) - ); -}); - -test('GoToMarker', () => { - const code = ` -////class A: -//// /*marker1*/pass - `; - - const { data, state } = parseAndGetTestState(code); - const marker = data.markerPositions.get('marker1'); - - state.goToMarker('marker1'); - assert.equal(state.lastKnownMarker, 'marker1'); - assert.equal(state.currentCaretPosition, marker!.position); - - state.goToMarker(marker); - assert.equal(state.lastKnownMarker, 'marker1'); - assert.equal(state.currentCaretPosition, marker!.position); - assert.equal(state.selectionEnd, -1); -}); - -test('GoToEachMarker', () => { - const code = ` -// @filename: file1.py -////class A: -//// /*marker1*/pass - -// @filename: file2.py -////class B: -//// /*marker2*/pass - `; - - const { data, state } = parseAndGetTestState(code); - const marker1 = data.markerPositions.get('marker1'); - const marker2 = data.markerPositions.get('marker2'); - - const results: number[] = []; - state.goToEachMarker([marker1!, marker2!], (m) => { - results.push(m.position); - }); - - assert.deepEqual(results, [marker1!.position, marker2!.position]); - - assert.equal(state.activeFile.fileName, marker2!.fileName); - assert.equal(state.currentCaretPosition, marker2!.position); - assert.equal(state.selectionEnd, -1); -}); - -test('Markers', () => { - const code = ` -// @filename: file1.py -////class A: -//// /*marker1*/pass - -// @filename: file2.py -////class B: -//// /*marker2*/pass - `; - - const { data, state } = parseAndGetTestState(code); - const marker1 = data.markerPositions.get('marker1'); - - assert.deepEqual(state.getMarkerName(marker1!), 'marker1'); - assert.deepEqual( - state - .getMarkers() - .map((m) => state.getMarkerName(m)) - .sort(compareStringsCaseSensitive), - state.getMarkerNames().sort(compareStringsCaseSensitive) - ); -}); - -test('GoToPosition', () => { - const code = ` -// @filename: file1.py -////class A: -//// /*marker1*/pass - `; - - const { data, state } = parseAndGetTestState(code); - const marker1 = data.markerPositions.get('marker1'); - state.goToPosition(marker1!.position); - - assert.equal(state.currentCaretPosition, marker1!.position); - assert.equal(state.selectionEnd, -1); -}); - -test('select', () => { - const code = ` -// @filename: file1.py -/////*start*/class A: -//// class B: -//// def Test(self): -//// pass -//// -//// def Test2(self): -//// pass/*end*/ - `; - - const { data, state } = parseAndGetTestState(code); - - state.select('start', 'end'); - - assert.equal(state.currentCaretPosition, data.markerPositions.get('start')!.position); - assert.equal(state.selectionEnd, data.markerPositions.get('end')!.position); -}); - -test('selectAllInFile', () => { - const code = ` -// @filename: file1.py -/////*start*/class A: -//// class B: -//// def Test(self): -//// pass -//// -//// def Test2(self): -//// pass/*end*/ - `; - - const { data, state } = parseAndGetTestState(code); - state.selectAllInFile(data.files[0].fileName); - - assert.equal(state.currentCaretPosition, data.markerPositions.get('start')!.position); - assert.equal(state.selectionEnd, data.markerPositions.get('end')!.position); -}); - -test('selectRange', () => { - const code = ` -// @filename: file1.py -/////class A: -//// class B: -//// [|def Test(self): -//// pass|] -//// -//// def Test2(self): -//// pass - `; - - const { data, state } = parseAndGetTestState(code); - const range = data.ranges[0]; - - state.selectRange(range); - - assert.equal(state.activeFile.fileName, range.fileName); - assert.equal(state.currentCaretPosition, range.pos); - assert.equal(state.selectionEnd, range.end); -}); - -test('selectLine', () => { - const code = ` -// @filename: file1.py -/////class A: -//// class B: -////[| def Test(self):|] -//// pass -//// -//// def Test2(self): -//// pass - `; - - const { data, state } = parseAndGetTestState(code); - const range = data.ranges[0]; - - state.selectLine(2); - - assert.equal(state.currentCaretPosition, range.pos); - assert.equal(state.selectionEnd, range.end); -}); - -test('goToEachRange', () => { - const code = ` -// @filename: file1.py -/////class A: -//// class B: -//// [|def Test(self):|] -//// pass -//// -//// def Test2(self): -//// [|pass|] - `; - - const { state } = parseAndGetTestState(code); - - const results: Range[] = []; - state.goToEachRange((r) => { - assert.equal(state.activeFile.fileName, r.fileName); - results.push(r); - }); - - assert.deepEqual(results, [state.getRanges()[0], state.getRanges()[1]]); -}); - -test('getRangesInFile', () => { - const code = ` -// @filename: file1.py -/////class A: -//// class B: -//// [|def Test(self):|] -//// pass - -// @filename: file2.py -//// def Test2(self): -//// [|pass|] - `; - - const { data, state } = parseAndGetTestState(code); - - assert.deepEqual( - state.getRangesInFile(data.files[0].fileName), - data.ranges.filter((r) => r.fileName === data.files[0].fileName) - ); -}); - -test('rangesByText', () => { - const code = ` -// @filename: file1.py -/////class A: -//// class B: -//// [|def Test(self):|] -//// pass - -// @filename: file2.py -//// def Test2(self): -//// [|pass|] - `; - - const { data, state } = parseAndGetTestState(code); - const map = state.getRangesByText(); - - assert.deepEqual(map.get('def Test(self):'), [data.ranges[0]]); - assert.deepEqual(map.get('pass'), [data.ranges[1]]); -}); - -test('moveCaretRight', () => { - const code = ` -// @filename: file1.py -/////class A: -//// class B: -//// /*position*/def Test(self): -//// pass -//// -//// def Test2(self): -//// pass - `; - - const { data, state } = parseAndGetTestState(code); - const marker = data.markerPositions.get('position')!; - - state.goToBOF(); - assert.equal(state.currentCaretPosition, 0); - - state.goToEOF(); - assert.equal(state.currentCaretPosition, data.files[0].content.length); - - state.goToPosition(marker.position); - state.moveCaretRight('def'.length); - - assert.equal(state.currentCaretPosition, marker.position + 'def'.length); - assert.equal(state.selectionEnd, -1); -}); - -test('runFourSlashTestContent', () => { - const code = ` -/// - -// @filename: file1.py -//// class A: -//// class B: -//// /*position*/def Test(self): -//// pass -//// -//// def Test2(self): -//// pass - -helper.getMarkerByName("position"); - `; - - runFourSlashTestContent(normalizeSlashes('/'), 'unused.py', code); -}); - -test('VerifyDiagnosticsTest1', () => { - const code = ` -/// - -// @filename: dataclass1.py -//// # This sample validates the Python 3.7 data class feature. -//// -//// from typing import NamedTuple, Optional -//// -//// class Other: -//// pass -//// -//// class DataTuple(NamedTuple): -//// def _m(self): -//// pass -//// id: int -//// aid: Other -//// valll: str = '' -//// name: Optional[str] = None -//// -//// d1 = DataTuple(id=1, aid=Other()) -//// d2 = DataTuple(id=1, aid=Other(), valll='v') -//// d3 = DataTuple(id=1, aid=Other(), name='hello') -//// d4 = DataTuple(id=1, aid=Other(), name=None) -//// id = d1.id -//// -//// # This should generate an error because the name argument -//// # is the incorrect type. -//// d5 = DataTuple(id=1, aid=Other(), name=[|{|"category": "error"|}3|]) -//// -//// # This should generate an error because aid is a required -//// # parameter and is missing an argument here. -//// d6 = [|{|"category": "error"|}DataTuple(id=1, name=None|]) - -helper.verifyDiagnostics(); - `; - - runFourSlashTestContent(factory.srcFolder, 'unused.py', code); -}); - -test('VerifyDiagnosticsTest2', () => { - const code = ` - - -//// # This sample tests the handling of the @dataclass decorator. -//// -//// from dataclasses import dataclass, InitVar -//// -//// @dataclass -//// class Bar(): -//// bbb: int -//// ccc: str -//// aaa = 'string' -//// -//// bar1 = Bar(bbb=5, ccc='hello') -//// bar2 = Bar(5, 'hello') -//// bar3 = Bar(5, 'hello', 'hello2') -//// print(bar3.bbb) -//// print(bar3.ccc) -//// print(bar3.aaa) -//// -//// # This should generate an error because ddd -//// # isn't a declared value. -//// bar = Bar(bbb=5, [|/*marker1*/ddd|]=5, ccc='hello') -//// -//// # This should generate an error because the -//// # parameter types don't match. -//// bar = Bar([|/*marker2*/'hello'|], 'goodbye') -//// -//// # This should generate an error because a parameter -//// # is missing. -//// bar = [|/*marker3*/Bar(2)|] -//// -//// # This should generate an error because there are -//// # too many parameters. -//// bar = Bar(2, 'hello', 'hello', [|/*marker4*/4|]) -//// -//// -//// @dataclass -//// class Baz1(): -//// bbb: int -//// aaa = 'string' -//// -//// # This should generate an error because variables -//// # with no default cannot come after those with -//// # defaults. -//// [|/*marker5*/ccc|]: str -//// -//// @dataclass -//// class Baz2(): -//// aaa: str -//// ddd: InitVar[int] = 3 - -helper.verifyDiagnostics({ - "marker1": { category: "error", message: "No parameter named 'ddd'" }, - "marker2": { category: "error", message: "Argument of type 'Literal['hello']' cannot be assigned to parameter 'bbb' of type 'int'\\n 'str' is incompatible with 'int'" }, - "marker3": { category: "error", message: "Argument missing for parameter 'ccc'" }, - "marker4": { category: "error", message: "Expected 3 positional arguments" }, - "marker5": { category: "error", message: "Data fields without default value cannot appear after data fields with default values" }, -}); - `; - - runFourSlashTestContent(factory.srcFolder, 'unused.py', code); -}); diff --git a/packages/vipeye-internal/src/tests/typeServer/notebook.typeServer.test.ts b/packages/vipeye-internal/src/tests/typeServer/notebook.typeServer.test.ts deleted file mode 100644 index b5d251b6..00000000 --- a/packages/vipeye-internal/src/tests/typeServer/notebook.typeServer.test.ts +++ /dev/null @@ -1,368 +0,0 @@ -/* - * notebook.typeServer.test.ts - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * - * Tests for the type server's notebook cell-chain maintenance (NotebookUriMapper + - * createNotebookData / openNotebookCellChain / updateNotebookStructure). - * - * This is a Vipeye-native port of Pylance's `notebook.common.ts`. It exercises only the - * cell-chain core functions against a real AnalyzerService; it does not drive the notebook - * document handler (which just sequences calls to these same functions). - */ - -import { - DidChangeNotebookDocumentParams, - NotebookCellArrayChange, - NotebookCellKind, - TextDocumentIdentifier, - TextDocumentItem, -} from 'vscode-languageserver-protocol'; - -import { AnalyzerService } from '../../analyzer/service'; -import { IPythonMode } from '../../analyzer/sourceFile'; -import { CommandLineOptions } from '../../common/commandLineOptions'; -import { NullConsole } from '../../common/console'; -import { DiagnosticCategory } from '../../common/diagnostic'; -import { normalizeSlashes } from '../../common/pathUtils'; -import { ServiceKeys } from '../../common/serviceKeys'; -import { createServiceProvider } from '../../common/serviceProviderExtensions'; -import { Uri } from '../../common/uri/uri'; -import { ViperFileSystem } from '../../viperFileSystem'; -import { createNotebookData, openNotebookCellChain, updateNotebookStructure } from '../../typeServer/notebookCellChain'; -import { NotebookUriMapper } from '../../typeServer/notebookUriMapper'; -import { createInitStatus, WellKnownWorkspaceKinds, Workspace } from '../../workspaceFactory'; -import * as host from '../harness/testHost'; -import { createFromFileSystem } from '../harness/vfs/factory'; - -describe('updateNotebookStructure Tests (typeServer mode)', () => { - const testFileSystem = createFromFileSystem(host.HOST, /* ignoreCase */ false, { - cwd: normalizeSlashes('/'), - }); - const fileSystem = new ViperFileSystem(testFileSystem); - const serviceProvider = createServiceProvider(testFileSystem, fileSystem); - let uriMapper: NotebookUriMapper; - let analyzerService: AnalyzerService; - let workspace: Workspace; - const notebookUriString = 'file:///notebook.ipynb'; - const notebookUri = Uri.parse(notebookUriString, serviceProvider); - - beforeEach(() => { - uriMapper = new NotebookUriMapper(serviceProvider.get(ServiceKeys.caseSensitivityDetector)); - analyzerService = new AnalyzerService('name', serviceProvider, { - console: new NullConsole(), - shouldRunAnalysis: () => true, - }); - - // Don't run analysis automatically. - const options = new CommandLineOptions(normalizeSlashes('/src'), false); - options.languageServerSettings.enableAmbientAnalysis = false; - analyzerService.setOptions(options); - - workspace = { - workspaceName: 'name', - rootUri: Uri.file('/src', serviceProvider), - kinds: [WellKnownWorkspaceKinds.Regular], - service: analyzerService, - disableLanguageServices: false, - disableOrganizeImports: false, - disableWorkspaceSymbol: false, - isInitialized: createInitStatus(), - searchPathsToWatch: [], - disableTaggedHints: false, - }; - }); - - afterEach(() => { - analyzerService.dispose(); - }); - - test('delete cell', () => { - const nd = _setupWorkspace(notebookUriString, [ - _createCellUri('one'), - _createCellUri('two'), - _createCellUri('three'), - ]); - - const structure = { - array: { start: 1, deleteCount: 1 }, - didOpen: undefined, - didClose: [{ uri: _createCellUri('two') }], - }; - _applyChange(notebookUriString, structure); - - updateNotebookStructure(structure, nd, uriMapper, workspace); - - _verifyUris(nd.mappedCellUris, [_createCellFilePath('one'), _createCellFilePath('three')]); - expect(analyzerService.hasSourceFile(_createCellFilePath('two'))).toBe(false); - }); - - test('delete top cell', () => { - const nd = _setupWorkspace(notebookUriString, [ - _createCellUri('one'), - _createCellUri('two'), - _createCellUri('three'), - ]); - - const structure = { - array: { start: 0, deleteCount: 1 }, - didOpen: undefined, - didClose: [{ uri: _createCellUri('one') }], - }; - _applyChange(notebookUriString, structure); - - updateNotebookStructure(structure, nd, uriMapper, workspace); - - _verifyUris(nd.mappedCellUris, [_createCellFilePath('two'), _createCellFilePath('three')]); - expect(analyzerService.hasSourceFile(_createCellFilePath('one'))).toBe(false); - }); - - test('insert cell', () => { - const nd = _setupWorkspace(notebookUriString, [_createCellUri('one'), _createCellUri('two')]); - const structure = { - array: { - cells: [{ kind: NotebookCellKind.Code, document: _createCellUri('new') }], - start: 1, - deleteCount: 0, - }, - didOpen: [{ uri: _createCellUri('new'), languageId: 'python', version: 1, text: '' }], - didClose: undefined, - }; - _applyChange(notebookUriString, structure); - - updateNotebookStructure(structure, nd, uriMapper, workspace); - - _verifyUris(nd.mappedCellUris, [ - _createCellFilePath('one'), - _createCellFilePath('new'), - _createCellFilePath('two'), - ]); - expect(analyzerService.hasSourceFile(_createCellFilePath('new'))).toBe(true); - expect(analyzerService.getChainedUri(_createCellFilePath('new'))?.equals(_createCellFilePath('one'))).toBe( - true - ); - }); - - test('reorder cell', () => { - const nd = _setupWorkspace(notebookUriString, [ - _createCellUri('one'), - _createCellUri('two'), - _createCellUri('three'), - ]); - const structure = { - array: { - cells: [ - { kind: NotebookCellKind.Code, document: _createCellUri('three') }, - { kind: NotebookCellKind.Code, document: _createCellUri('two') }, - ], - start: 1, - deleteCount: 2, - }, - didOpen: undefined, - didClose: undefined, - }; - _applyChange(notebookUriString, structure); - - updateNotebookStructure(structure, nd, uriMapper, workspace); - - _verifyUris(nd.mappedCellUris, [ - _createCellFilePath('one'), - _createCellFilePath('three'), - _createCellFilePath('two'), - ]); - expect(analyzerService.getChainedUri(_createCellFilePath('two'))?.equals(_createCellFilePath('three'))).toBe( - true - ); - expect(analyzerService.getChainedUri(_createCellFilePath('three'))?.equals(_createCellFilePath('one'))).toBe( - true - ); - }); - - test('repair if duplicate cell detected', () => { - const nd = _setupWorkspace(notebookUriString, [_createCellUri('one'), _createCellUri('two')]); - const structure = { - array: { - cells: [{ kind: NotebookCellKind.Code, document: _createCellUri('one') }], - start: 2, - deleteCount: 0, - }, - didOpen: [{ uri: _createCellUri('one'), languageId: 'python', version: 1, text: '' }], - didClose: undefined, - }; - _applyChange(notebookUriString, structure); - - // Should not throw; instead it should repair the chain by removing the duplicate. - expect(() => updateNotebookStructure(structure, nd, uriMapper, workspace)).not.toThrow(); - // The duplicate entry should have been removed, leaving only the original two cells. - _verifyUris(nd.mappedCellUris, [_createCellFilePath('one'), _createCellFilePath('two')]); - }); - - test('delete last cell', () => { - const nd = _setupWorkspace(notebookUriString, [ - _createCellUri('one'), - _createCellUri('two'), - _createCellUri('three'), - ]); - - const structure = { - array: { start: 2, deleteCount: 1 }, - didOpen: undefined, - didClose: [{ uri: _createCellUri('three') }], - }; - _applyChange(notebookUriString, structure); - - updateNotebookStructure(structure, nd, uriMapper, workspace); - - _verifyUris(nd.mappedCellUris, [_createCellFilePath('one'), _createCellFilePath('two')]); - expect(analyzerService.hasSourceFile(_createCellFilePath('three'))).toBe(false); - // The chain should remain intact after the last cell is deleted: prefix → one → two - expect(analyzerService.getChainedUri(_createCellFilePath('two'))?.equals(_createCellFilePath('one'))).toBe( - true - ); - expect(analyzerService.getChainedUri(_createCellFilePath('one'))?.equals(nd.prefixCellUri)).toBe(true); - }); - - test('delete and create cells', () => { - const nd = _setupWorkspace(notebookUriString, [_createCellUri('one')]); - - const structure = { - array: { - cells: [{ kind: NotebookCellKind.Code, document: _createCellUri('new') }], - start: 0, - deleteCount: 1, - }, - didOpen: [{ uri: _createCellUri('new'), languageId: 'python', version: 1, text: '' }], - didClose: [{ uri: _createCellUri('one') }], - }; - _applyChange(notebookUriString, structure); - - updateNotebookStructure(structure, nd, uriMapper, workspace); - - _verifyUris(nd.mappedCellUris, [_createCellFilePath('new')]); - expect(analyzerService.hasSourceFile(_createCellFilePath('new'))).toBe(true); - expect(analyzerService.getChainedUri(_createCellFilePath('new'))?.equals(nd.prefixCellUri)).toBe(true); - }); - - test('unused symbol in chained source file', () => { - _setupWorkspace(notebookUriString, [_createCellUri('one'), _createCellUri('two'), _createCellUri('three')]); - - const source1 = workspace.service.test_program.getBoundSourceFileInfo(_createCellFilePath('one'))!; - const source2 = workspace.service.test_program.getBoundSourceFileInfo(_createCellFilePath('two'))!; - const source3 = workspace.service.test_program.getBoundSourceFileInfo(_createCellFilePath('three'))!; - - // Since we created the workspace for tests, some flags that would be set when - // workspace.getSettings is called are not set. Manually set them here. - source1.isTracked = true; - source2.isTracked = true; - source3.isTracked = true; - - _updateCellContent('one', 'import os'); - expect( - source1.sourceFile - .getDiagnostics(workspace.service.getConfigOptions()) - ?.some((d) => d.category === DiagnosticCategory.UnusedCode) - ).toBe(true); - - _updateCellContent('two', 'os'); - expect( - source1.sourceFile - .getDiagnostics(workspace.service.getConfigOptions()) - ?.some((d) => d.category === DiagnosticCategory.UnusedCode) - ).toBe(false); - - _updateCellContent('one', 'import os\nimport sys'); - expect( - source1.sourceFile - .getDiagnostics(workspace.service.getConfigOptions()) - ?.some((d) => d.category === DiagnosticCategory.UnusedCode) - ).toBe(true); - - _updateCellContent('three', 'sys'); - expect( - source1.sourceFile - .getDiagnostics(workspace.service.getConfigOptions()) - ?.some((d) => d.category === DiagnosticCategory.UnusedCode) - ).toBe(false); - - _updateCellContent('two', ''); - expect( - source1.sourceFile - .getDiagnostics(workspace.service.getConfigOptions()) - ?.some((d) => d.category === DiagnosticCategory.UnusedCode) - ).toBe(true); - }); - - function _updateCellContent(cellName: string, text: string) { - const filePath = _createCellFilePath(cellName); - const source = workspace.service.test_program.getBoundSourceFileInfo(filePath)!; - - workspace.service.updateOpenFileContents( - filePath, - (source.sourceFile.getClientVersion() ?? 0) + 1, - text, - IPythonMode.CellDocs - ); - - while (workspace.service.test_program.analyze()); - } - - function _setupWorkspace(notebookUri: string, cellFileUris: string[]) { - const cells = cellFileUris.map((uri) => TextDocumentItem.create(uri, 'python', 1, '')); - const params = { - notebookDocument: { - notebookType: 'jupyter-notebook', - uri: notebookUri, - version: 1, - cells: cells.map((cell) => ({ kind: NotebookCellKind.Code, document: cell.uri })), - }, - cellTextDocuments: cells, - }; - - const notebookPath = uriMapper.parseNotebookOpen(params); - const notebookData = createNotebookData( - notebookPath, - cells, - uriMapper, - serviceProvider.get(ServiceKeys.caseSensitivityDetector) - ); - openNotebookCellChain(cells, notebookData, uriMapper, workspace); - return notebookData; - } - - function _applyChange( - notebookUri: string, - structure: { - array: NotebookCellArrayChange; - didOpen?: TextDocumentItem[]; - didClose?: TextDocumentIdentifier[]; - } - ) { - const params: DidChangeNotebookDocumentParams = { - notebookDocument: { - uri: notebookUri, - version: 2, - }, - change: { - cells: { - structure, - }, - }, - }; - uriMapper.parseNotebookChange(params); - } - - function _createCellUri(cell: string) { - return `vscode-notebook-cell:notebook.ipynb#${cell}`; - } - - function _createCellFilePath(cell: string) { - return notebookUri.addExtension('.py').withFragment(cell); - } - - function _verifyUris(actual: Uri[], expected: Uri[]) { - expect(actual.length).toBe(expected.length); - for (let i = 0; i < actual.length; i++) { - expect(actual[i].equals(expected[i])).toBe(true); - } - } -}); diff --git a/packages/vipeye-internal/src/tests/typeServer/typeServer.inProc.test.ts b/packages/vipeye-internal/src/tests/typeServer/typeServer.inProc.test.ts deleted file mode 100644 index 89b92033..00000000 --- a/packages/vipeye-internal/src/tests/typeServer/typeServer.inProc.test.ts +++ /dev/null @@ -1,269 +0,0 @@ -/* - * typeServer.inProc.test.ts - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * - * In-process protocol tests for the Viper type server (TSP). - * - * These exercise the server-side request handlers end-to-end over a real JSON-RPC - * connection (in-memory streams), including the `_onGetType` conversion path - * (fromProtocolNode -> evaluate via the sync->async adapter -> ProtocolTypeFactory). - * - * Ported from Pylance's `typeServer.inProc.test.ts`. The Pylance round-trip tests that - * reconstruct a Vipeye `Type` from the protocol `Type` depend on the client-side consumer - * stack (ExternalProgram / snapshotSync), which stays in Pylance, so they are intentionally - * not ported here. Instead these tests assert on the protocol-level responses. - */ - -import assert from 'assert'; - -import { TypeServerProtocol } from '../../typeServer/protocol/typeServerProtocol'; -import { initializeDependenciesForInProcTests, withInProcTypeServer } from './inProcTypeServerTestUtils'; - -jest.setTimeout(120000); - -/** Extract the class name from a protocol Type, if it's a ClassType with a regular declaration. */ -function getClassTypeName(type: TypeServerProtocol.Type | undefined): string | undefined { - if (!type || type.kind !== TypeServerProtocol.TypeKind.Class) { - return undefined; - } - const classType = type as TypeServerProtocol.ClassType; - if (classType.declaration.kind === TypeServerProtocol.DeclarationKind.Regular) { - return classType.declaration.name; - } - return undefined; -} - -describe('TypeServer in-proc protocol', () => { - beforeAll(async () => { - await initializeDependenciesForInProcTests(); - }); - - test('initialize + get supported protocol version + snapshot', async () => { - const code = ` -// @filename: main.py -//// x = 1 -`; - - await withInProcTypeServer(code, async (context) => { - const version = await context.sendRequest(TypeServerProtocol.GetSupportedProtocolVersionRequest.type); - assert(version.length > 0); - const snapshot = await context.refreshSnapshot(); - assert(snapshot >= 0); - }); - }); - - test('resolveImport resolves a local module', async () => { - const code = ` -// @filename: main.py -//// # /*src*/ -//// import foo -//// x = foo.value -// @filename: foo.py -//// value = 1 -`; - - await withInProcTypeServer(code, async (context) => { - const resolvedUri = await context.sendRequestWithSnapshot(TypeServerProtocol.ResolveImportRequest.type, { - sourceUri: context.getFileUriForMarker('src').toString(), - moduleDescriptor: { leadingDots: 0, nameParts: ['foo'] }, - }); - - assert(resolvedUri !== undefined); - assert(resolvedUri.endsWith('/src/foo.py')); - }); - }); - - test('getPythonSearchPaths returns an array', async () => { - const code = ` -// @filename: main.py -//// # /*src*/ -//// x = 1 -`; - - await withInProcTypeServer(code, async (context) => { - const paths = await context.sendRequestWithSnapshot(TypeServerProtocol.GetPythonSearchPathsRequest.type, { - fromUri: context.getFileUriForMarker('src').getDirectory().toString(), - }); - - assert(Array.isArray(paths)); - assert(paths.every((p) => typeof p === 'string')); - }); - }); - - test('resolveImport resolves a relative module', async () => { - const code = ` -// @filename: pkg/__init__.py -//// -// @filename: pkg/main.py -//// # /*src*/ -//// from . import foo -//// x = foo.value -// @filename: pkg/foo.py -//// value = 1 -`; - - await withInProcTypeServer(code, async (context) => { - const resolvedUri = await context.sendRequestWithSnapshot(TypeServerProtocol.ResolveImportRequest.type, { - sourceUri: context.getFileUriForMarker('src').toString(), - moduleDescriptor: { leadingDots: 1, nameParts: ['foo'] }, - }); - - assert(resolvedUri !== undefined); - assert(resolvedUri.endsWith('/src/pkg/foo.py')); - }); - }); - - test('resolveImport resolves stdlib module from typeshed-fallback', async () => { - const code = ` -// @filename: main.py -//// # /*src*/ -//// import typing -//// x = typing.Any -`; - - await withInProcTypeServer(code, async (context) => { - const resolvedUri = await context.sendRequestWithSnapshot(TypeServerProtocol.ResolveImportRequest.type, { - sourceUri: context.getFileUriForMarker('src').toString(), - moduleDescriptor: { leadingDots: 0, nameParts: ['typing'] }, - }); - - assert(resolvedUri !== undefined); - // The exact URI scheme can vary depending on FS mapping; the key is that this came from typeshed. - assert(resolvedUri.includes('/typeshed-fallback/stdlib/typing.pyi')); - }); - }); - - test('resolveImport resolves builtins module from typeshed-fallback', async () => { - const code = ` -// @filename: main.py -//// # /*src*/ -//// import builtins -//// x = builtins.int -`; - - await withInProcTypeServer(code, async (context) => { - const resolvedUri = await context.sendRequestWithSnapshot(TypeServerProtocol.ResolveImportRequest.type, { - sourceUri: context.getFileUriForMarker('src').toString(), - moduleDescriptor: { leadingDots: 0, nameParts: ['builtins'] }, - }); - - assert(resolvedUri !== undefined); - assert(resolvedUri.includes('/typeshed-fallback/stdlib/builtins.pyi')); - }); - }); - - test('resolveImport resolves stdlib submodule from typeshed-fallback', async () => { - const code = ` -// @filename: main.py -//// # /*src*/ -//// import collections.abc -//// x = collections.abc.Iterable -`; - - await withInProcTypeServer(code, async (context) => { - const resolvedUri = await context.sendRequestWithSnapshot(TypeServerProtocol.ResolveImportRequest.type, { - sourceUri: context.getFileUriForMarker('src').toString(), - moduleDescriptor: { leadingDots: 0, nameParts: ['collections', 'abc'] }, - }); - - assert(resolvedUri !== undefined); - assert(resolvedUri.includes('/typeshed-fallback/stdlib/collections/abc.pyi')); - }); - }); - - test('getComputedType returns a type for a node', async () => { - const code = ` -// @filename: main.py -//// def takes_int(x: int) -> None: -//// pass -//// -//// [|/*arg*/__arg__|] = 1 -//// takes_int(__arg__) -`; - - await withInProcTypeServer(code, async (context) => { - await context.openFileForMarker('arg'); - const arg: TypeServerProtocol.Node = context.getNodeForMarker('arg'); - - await context.refreshSnapshot(); - const type = await context.sendRequestWithSnapshot(TypeServerProtocol.GetComputedTypeRequest.type, { - arg, - }); - - assert(type !== undefined); - assert(typeof type.id === 'number'); - assert.strictEqual(type.kind, TypeServerProtocol.TypeKind.Class); - assert.strictEqual(getClassTypeName(type), 'int'); - }); - }); - - test('getExpectedType returns a type for a node', async () => { - const code = ` -// @filename: main.py -//// class C: -//// pass -//// -//// x: C = [|/*expr*/C()|] -`; - - await withInProcTypeServer(code, async (context) => { - await context.openFileForMarker('expr'); - const arg: TypeServerProtocol.Node = context.getNodeForMarker('expr'); - - await context.refreshSnapshot(); - const type = await context.sendRequestWithSnapshot(TypeServerProtocol.GetExpectedTypeRequest.type, { - arg, - }); - - assert(type !== undefined); - assert(typeof type.id === 'number'); - assert.strictEqual(type.kind, TypeServerProtocol.TypeKind.Class); - assert.strictEqual(getClassTypeName(type), 'C'); - }); - }); - - test('getDeclaredType returns a type for an annotated symbol', async () => { - const code = ` -// @filename: main.py -//// [|/*decl*/__declared__|]: str = 'x' -`; - - await withInProcTypeServer(code, async (context) => { - await context.openFileForMarker('decl'); - const arg: TypeServerProtocol.Node = context.getNodeForMarker('decl'); - - await context.refreshSnapshot(); - const type = await context.sendRequestWithSnapshot(TypeServerProtocol.GetDeclaredTypeRequest.type, { - arg, - }); - - assert(type !== undefined); - assert(typeof type.id === 'number'); - assert.strictEqual(type.kind, TypeServerProtocol.TypeKind.Class); - assert.strictEqual(getClassTypeName(type), 'str'); - }); - }); - - test('snapshotChanged notification is delivered', async () => { - const code = ` -// @filename: main.py -//// [|/*open*/|]x = 1 -`; - - await withInProcTypeServer(code, async (context) => { - const initialSnapshot = await context.refreshSnapshot(); - const received = new Promise<{ old: number; new: number }>((resolve) => { - context.onNotification(TypeServerProtocol.SnapshotChangedNotification.type, (params) => { - resolve(params); - }); - }); - - await context.openFileForMarker('open'); - - const params = await received; - assert(params.new > params.old); - assert(params.old >= initialSnapshot); - }); - }); -}); diff --git a/packages/vipeye-internal/src/tests/wildcardImportPackageMerge.test.ts b/packages/vipeye-internal/src/tests/wildcardImportPackageMerge.test.ts deleted file mode 100644 index 67e4b8b5..00000000 --- a/packages/vipeye-internal/src/tests/wildcardImportPackageMerge.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -/// - -/* - * wildcardImportPackageMerge.test.ts - * - * Regression coverage for multipart package members that survive wildcard-import merges. - */ - -import { CompletionItemKind, MarkupKind } from 'vscode-languageserver-types'; - -import { DefinitionFilter } from '../languageService/definitionProvider'; -import { parseAndGetTestState } from './harness/fourslash/testState'; -import { verifyReferencesAtPosition } from './testStateUtils'; - -const wildcardImportPackageMergeCode = ` -// @filename: mylib/__init__.py -//// - -// @filename: mylib/a.py -//// [|/*defA*/|]# module a -//// def do_something(value: str) -> None: -//// pass - -// @filename: mylib/b.py -//// [|/*defB*/|]# module b -//// def do_other() -> None: -//// pass - -// @filename: my_common_stuffs.py -//// import mylib.[|/*importB*/b|] -//// BLA = '123' - -// @filename: test.py -//// import mylib.[|/*importA*/a|] -//// -//// from my_common_stuffs import * -//// -//// mylib.[|/*markerA*/a|].do_something(BLA) -//// mylib.[|/*markerB*/b|].do_other() -//// mylib./*completionMarker*/ -`; - -function createState() { - return parseAndGetTestState(wildcardImportPackageMergeCode).state; -} - -function getExpectedDefinition(state: ReturnType, markerName: string) { - return { - path: state.getMarkerByName(markerName).fileName, - range: state.getPositionRange(markerName), - }; -} - -test('wildcard import package merge - go to definition for sibling modules', () => { - const state = createState(); - - state.verifyFindDefinitions( - { - markerA: { - definitions: [getExpectedDefinition(state, 'defA')], - }, - markerB: { - definitions: [getExpectedDefinition(state, 'defB')], - }, - }, - DefinitionFilter.PreferSource - ); -}); - -test('wildcard import package merge - hover for sibling modules', () => { - const state = createState(); - const marker = state.getMarkerByName('markerA'); - state.openFile(marker.fileName); - - state.verifyHover('markdown', { - markerA: '```python\n(module) a\n```', - markerB: '```python\n(module) b\n```', - }); -}); - -test('wildcard import package merge - completion exposes sibling modules', async () => { - const state = createState(); - const marker = state.getMarkerByName('completionMarker'); - state.openFile(marker.fileName); - - await state.verifyCompletion('included', MarkupKind.Markdown, { - completionMarker: { - completions: [ - { - kind: CompletionItemKind.Module, - label: 'a', - }, - { - kind: CompletionItemKind.Module, - label: 'b', - }, - ], - }, - }); -}); - -test('wildcard import package merge - find references for sibling modules', () => { - const state = createState(); - - for (const symbolName of ['a', 'b']) { - const ranges = state.getRangesByText().get(symbolName)!; - for (const range of ranges) { - verifyReferencesAtPosition( - state.program, - state.configOptions, - symbolName, - range.fileName, - range.pos, - ranges - ); - } - } -}); diff --git a/packages/vipeye-internal/src/tests/workspaceEditUtils.test.ts b/packages/vipeye-internal/src/tests/workspaceEditUtils.test.ts deleted file mode 100644 index f2cc97d5..00000000 --- a/packages/vipeye-internal/src/tests/workspaceEditUtils.test.ts +++ /dev/null @@ -1,490 +0,0 @@ -/* - * workspaceEditUtils.test.ts - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * - * test workspaceEditUtils - */ - -import * as assert from 'assert'; -import { CreateFile, DeleteFile, RenameFile, TextDocumentEdit, WorkspaceEdit } from 'vscode-languageserver-types'; - -import { CancellationToken } from 'vscode-languageserver'; -import { AnalyzerService } from '../analyzer/service'; -import { IPythonMode } from '../analyzer/sourceFile'; -import { combinePaths, getDirectoryPath } from '../common/pathUtils'; -import { Uri } from '../common/uri/uri'; -import { - applyWorkspaceEdit, - convertToWorkspaceEdit, - generateWorkspaceEdit, - hasWorkspaceEditChanges, -} from '../common/workspaceEditUtils'; -import { AnalyzerServiceExecutor } from '../languageService/analyzerServiceExecutor'; -import { TestLanguageService } from './harness/fourslash/testLanguageService'; -import { TestState, parseAndGetTestState } from './harness/fourslash/testState'; -import { verifyWorkspaceEdit } from './harness/fourslash/workspaceEditTestUtils'; - -test('test applyWorkspaceEdits changes', async () => { - const code = ` -// @filename: test.py -//// [|/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - const cloned = await getClonedService(state); - const range = state.getRangeByMarkerName('marker')!; - - const fileChanged = new Map(); - applyWorkspaceEditToService( - cloned, - { - changes: { - [range.fileUri.toString()]: [ - { - range: state.convertPositionRange(range), - newText: 'Text Changed', - }, - ], - }, - }, - fileChanged - ); - - assert.strictEqual(fileChanged.size, 1); - assert.strictEqual(cloned.test_program.getSourceFile(range.fileUri)?.getFileContent(), 'Text Changed'); -}); - -test('test edit mode for workspace', async () => { - const code = ` -// @filename: test.py -//// [|/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - const range = state.getRangeByMarkerName('marker')!; - const addedFileUri = Uri.file(combinePaths(getDirectoryPath(range.fileName), 'test2.py'), state.serviceProvider); - const edits = state.workspace.service.runEditMode((program) => { - const fileChanged = new Map(); - applyWorkspaceEdit( - program, - { - documentChanges: [ - TextDocumentEdit.create( - { - uri: range.fileUri.toString(), - version: null, - }, - [ - { - range: state.convertPositionRange(range), - newText: 'import sys', - }, - ] - ), - ], - }, - fileChanged - ); - - assert.strictEqual(fileChanged.size, 1); - const info = program.getSourceFileInfo(range.fileUri)!; - - program.analyzeFile(info.uri, CancellationToken.None); - assert.strictEqual(info.contents, 'import sys'); - assert.strictEqual(info.imports.length, 3); - - // Add a new file. - program.setFileOpened(addedFileUri, 0, '', { - ipythonMode: IPythonMode.None, - chainedFileUri: undefined, - }); - - applyWorkspaceEdit( - program, - { - documentChanges: [ - TextDocumentEdit.create( - { - uri: addedFileUri.toString(), - version: null, - }, - [ - { - range: { - start: { line: 0, character: 0 }, - end: { line: 0, character: 0 }, - }, - newText: 'import sys', - }, - ] - ), - ], - }, - fileChanged - ); - - applyWorkspaceEdit( - program, - { - documentChanges: [ - TextDocumentEdit.create( - { - uri: addedFileUri.toString(), - version: null, - }, - [ - { - range: { - start: { line: 0, character: 7 }, - end: { line: 0, character: 10 }, - }, - newText: 'os', - }, - ] - ), - ], - }, - fileChanged - ); - - const addedInfo = program.getSourceFileInfo(addedFileUri)!; - program.analyzeFile(addedInfo.uri, CancellationToken.None); - - assert.strictEqual(addedInfo.contents, 'import os'); - assert.strictEqual(addedInfo.imports.length, 3); - }, CancellationToken.None); - - // After leaving edit mode, we should be back to where we were. - const oldSourceFile = state.workspace.service.test_program.getSourceFile(range.fileUri); - state.workspace.service.backgroundAnalysisProgram.analyzeFile(oldSourceFile!.getUri(), CancellationToken.None); - - assert.strictEqual(oldSourceFile?.getFileContent(), ''); - assert.strictEqual(oldSourceFile.getImports().length, 2); - assert.strictEqual(edits.length, 2); - - assert.deepStrictEqual(edits[0].replacementText, 'import sys'); - assert.deepStrictEqual(edits[1].replacementText, 'import os'); - - const addedSourceFile = state.workspace.service.test_program.getSourceFile(addedFileUri); - - // The added file should not be there. - assert.ok(!addedSourceFile); -}); - -test('test applyWorkspaceEdits documentChanges', async () => { - const code = ` -// @filename: test.py -//// [|/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - const cloned = await getClonedService(state); - const range = state.getRangeByMarkerName('marker')!; - - const fileChanged = new Map(); - applyWorkspaceEditToService( - cloned, - { - documentChanges: [ - TextDocumentEdit.create( - { - uri: range.fileUri.toString(), - version: null, - }, - [ - { - range: state.convertPositionRange(range), - newText: 'Text Changed', - }, - ] - ), - ], - }, - fileChanged - ); - - assert.strictEqual(fileChanged.size, 1); - assert.strictEqual(cloned.test_program.getSourceFile(range.fileUri)?.getFileContent(), 'Text Changed'); -}); - -test('test generateWorkspaceEdits', async () => { - const code = ` -// @filename: test1.py -//// [|/*marker1*/|] - -// @filename: test2.py -//// [|/*marker2*/|] - `; - - const state = parseAndGetTestState(code).state; - const cloned = await getClonedService(state); - const range1 = state.getRangeByMarkerName('marker1')!; - - const fileChanged = new Map(); - applyWorkspaceEditToService( - cloned, - { - changes: { - [range1.fileUri.toString()]: [ - { - range: state.convertPositionRange(range1), - newText: 'Test1 Changed', - }, - ], - }, - }, - fileChanged - ); - - applyWorkspaceEditToService( - cloned, - { - documentChanges: [ - TextDocumentEdit.create( - { - uri: range1.fileUri.toString(), - version: null, - }, - [ - { - range: { start: { line: 0, character: 0 }, end: { line: 0, character: 5 } }, - newText: 'NewTest1', - }, - ] - ), - ], - }, - fileChanged - ); - - const range2 = state.getRangeByMarkerName('marker2')!; - applyWorkspaceEditToService( - cloned, - { - documentChanges: [ - TextDocumentEdit.create( - { - uri: range2.fileUri.toString(), - version: null, - }, - [ - { - range: state.convertPositionRange(range2), - newText: 'Test2 Changed', - }, - ] - ), - ], - }, - fileChanged - ); - - applyWorkspaceEditToService( - cloned, - { - changes: { - [range2.fileUri.toString()]: [ - { - range: { start: { line: 0, character: 0 }, end: { line: 0, character: 5 } }, - newText: 'NewTest2', - }, - ], - }, - }, - fileChanged - ); - - assert.strictEqual(fileChanged.size, 2); - - const actualEdits = generateWorkspaceEdit(state.workspace.service.fs, state.workspace.service, cloned, fileChanged); - verifyWorkspaceEdit( - { - changes: { - [range1.fileUri.toString()]: [ - { - range: state.convertPositionRange(range1), - newText: 'NewTest1 Changed', - }, - ], - [range2.fileUri.toString()]: [ - { - range: state.convertPositionRange(range1), - newText: 'NewTest2 Changed', - }, - ], - }, - }, - actualEdits - ); -}); - -test('test convertToWorkspaceEdit omits annotationId without changeAnnotations', () => { - const code = ` -// @filename: a.py -//// [|/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - const range = state.getRangeByMarkerName('marker')!; - - const renameTarget = Uri.file( - combinePaths(getDirectoryPath(range.fileName), 'a_renamed.py'), - state.serviceProvider - ); - const deleteTarget = Uri.file(combinePaths(getDirectoryPath(range.fileName), 'c.py'), state.serviceProvider); - - const editActions = { - edits: [ - { - fileUri: range.fileUri, - range: state.convertPositionRange(range), - replacementText: 'x', - }, - ], - fileOperations: [ - { - kind: 'create' as const, - fileUri: Uri.file(combinePaths(getDirectoryPath(range.fileName), 'b.py'), state.serviceProvider), - }, - { - kind: 'rename' as const, - oldFileUri: range.fileUri, - newFileUri: renameTarget, - }, - { - kind: 'delete' as const, - fileUri: deleteTarget, - }, - ], - }; - - const ws = convertToWorkspaceEdit(state.workspace.service.fs, editActions); - assert.strictEqual(ws.changeAnnotations, undefined); - const tde = ws.documentChanges!.find((d) => TextDocumentEdit.is(d)) as TextDocumentEdit; - const anyEdit = tde.edits[0] as any; - assert.strictEqual(anyEdit.annotationId, undefined); - const createOp = ws.documentChanges!.find((d) => (d as any).kind === 'create') as CreateFile; - assert.strictEqual(createOp.annotationId, undefined); - const renameOp = ws.documentChanges!.find((d) => (d as any).kind === 'rename') as RenameFile; - assert.strictEqual(renameOp.annotationId, undefined); - const deleteOp = ws.documentChanges!.find((d) => (d as any).kind === 'delete') as DeleteFile; - assert.strictEqual(deleteOp.annotationId, undefined); -}); - -test('test convertToWorkspaceEdit includes annotationId with changeAnnotations', () => { - const code = ` -// @filename: a.py -//// [|/*marker*/|] - `; - - const state = parseAndGetTestState(code).state; - const range = state.getRangeByMarkerName('marker')!; - - const renameTarget = Uri.file( - combinePaths(getDirectoryPath(range.fileName), 'a_renamed.py'), - state.serviceProvider - ); - const deleteTarget = Uri.file(combinePaths(getDirectoryPath(range.fileName), 'c.py'), state.serviceProvider); - - const editActions = { - edits: [ - { - fileUri: range.fileUri, - range: state.convertPositionRange(range), - replacementText: 'x', - }, - ], - fileOperations: [ - { - kind: 'create' as const, - fileUri: Uri.file(combinePaths(getDirectoryPath(range.fileName), 'b.py'), state.serviceProvider), - }, - { - kind: 'rename' as const, - oldFileUri: range.fileUri, - newFileUri: renameTarget, - }, - { - kind: 'delete' as const, - fileUri: deleteTarget, - }, - ], - }; - - const changeAnnotations = { - default: { label: 'label', description: 'desc', needsConfirmation: false }, - }; - - const ws = convertToWorkspaceEdit(state.workspace.service.fs, editActions, changeAnnotations, 'default'); - assert.ok(ws.changeAnnotations); - assert.ok(ws.changeAnnotations!['default']); - const tde = ws.documentChanges!.find((d) => TextDocumentEdit.is(d)) as TextDocumentEdit; - const anyEdit = tde.edits[0] as any; - assert.strictEqual(anyEdit.annotationId, 'default'); - const createOp = ws.documentChanges!.find((d) => (d as any).kind === 'create') as CreateFile; - assert.strictEqual(createOp.annotationId, 'default'); - const renameOp = ws.documentChanges!.find((d) => (d as any).kind === 'rename') as RenameFile; - assert.strictEqual(renameOp.annotationId, 'default'); - const deleteOp = ws.documentChanges!.find((d) => (d as any).kind === 'delete') as DeleteFile; - assert.strictEqual(deleteOp.annotationId, 'default'); -}); - -test('test hasWorkspaceEditChanges for empty workspace edits', () => { - assert.strictEqual(hasWorkspaceEditChanges({ changes: {} }), false); - assert.strictEqual(hasWorkspaceEditChanges({ changes: { 'file:///test.py': [] } }), false); - assert.strictEqual( - hasWorkspaceEditChanges({ - documentChanges: [ - TextDocumentEdit.create( - { - uri: 'file:///test.py', - version: null, - }, - [] - ), - ], - }), - false - ); -}); - -test('test hasWorkspaceEditChanges for text and file operations', () => { - const textEdit = { - range: { - start: { line: 0, character: 0 }, - end: { line: 0, character: 0 }, - }, - newText: 'x', - }; - - assert.strictEqual(hasWorkspaceEditChanges({ changes: { 'file:///test.py': [textEdit] } }), true); - assert.strictEqual( - hasWorkspaceEditChanges({ - documentChanges: [ - TextDocumentEdit.create( - { - uri: 'file:///test.py', - version: null, - }, - [textEdit] - ), - ], - }), - true - ); - assert.strictEqual(hasWorkspaceEditChanges({ documentChanges: [CreateFile.create('file:///created.py')] }), true); -}); - -function applyWorkspaceEditToService(service: AnalyzerService, edits: WorkspaceEdit, filesChanged: Map) { - const program = service.backgroundAnalysisProgram.program; - applyWorkspaceEdit(program, edits, filesChanged); -} - -async function getClonedService(state: TestState) { - return await AnalyzerServiceExecutor.cloneService( - new TestLanguageService(state.workspace, state.console, state.workspace.service.fs), - state.workspace, - { useBackgroundAnalysis: false } - ); -}