test(vipeye-internal): 删除剩余 18 个内联 .py 夹具套件,测试集转为 Viper-only
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
- 删除 completions/signatureHelp/diagnostics/service/languageServer/importResolver(+Support)/ importStatementUtils/documentSymbolCollector/chainedSourceFiles/sourceMapperUtils/workspaceEditUtils/ testState/classDeclaration/wildcardImportPackageMerge/privateImportUsage/typeServer 等 18 个套件 - 保留并加注 tests/fourslash/typings/fourslash.d.ts:其 declare namespace _ 仍被 harness/fourslash/testState.ts 引用,删除会导致 ts-jest TS2694 整批套件无法运行 验证: 全量串行 53 套件通过、1 跳过,732 用例全绿(0 失败)
This commit is contained in:
1 parent
a955a5356f
commit
04d5a32502
19 files changed
+455
-10345
No files matched your search
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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 }))
|
||||
)}`
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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<string, Range[]>;
|
||||
getFilteredRanges<T extends {}>(
|
||||
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<any>;
|
||||
verifyCommand(command: Command, files: { [filePath: string]: string }): Promise<any>;
|
||||
verifyInvokeCodeAction(
|
||||
map: {
|
||||
[marker: string]: { title: string; files?: { [filePath: string]: string }; edits?: TextEdit[] };
|
||||
},
|
||||
verifyCodeActionCount?: boolean
|
||||
): Promise<any>;
|
||||
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<void>;
|
||||
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';
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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}'`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)}`
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = `
|
||||
/// <reference path="typings/fourslash.d.ts" />
|
||||
|
||||
// @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 = `
|
||||
/// <reference path="typings/fourslash.d.ts" />
|
||||
|
||||
// @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);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,118 +0,0 @@
|
||||
/// <reference types="jest" />
|
||||
|
||||
/*
|
||||
* 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<typeof createState>, 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
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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<string, Uri>();
|
||||
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<string, Uri>();
|
||||
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<string, Uri>();
|
||||
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<string, Uri>();
|
||||
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<string, Uri>) {
|
||||
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 }
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user