Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions packages/pyright-internal/src/analyzer/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,12 +360,21 @@ export class Program {
return sourceFileInfo.sourceFile;
}

// Detect py.typed status if not explicitly provided. This ensures that
// files from py.typed packages are correctly marked even when added
// directly to check paths (e.g., via command line).
let effectiveIsInPyTypedPackage = isInPyTypedPackage;
if (!isInPyTypedPackage) {
const moduleImportInfo = this._getModuleImportInfoForFile(fileUri);
effectiveIsInPyTypedPackage = moduleImportInfo.isThirdPartyPyTypedPresent;
}

const sourceFile = this._sourceFileFactory.createSourceFile(
this.serviceProvider,
fileUri,
(uri) => this._getModuleName(uri),
isThirdPartyImport,
isInPyTypedPackage,
effectiveIsInPyTypedPackage,
this._editModeTracker,
this._console,
this._logTracker
Expand All @@ -374,7 +383,7 @@ export class Program {
sourceFile,
sourceFile.isTypingStubFile() || sourceFile.isTypeshedStubFile() || sourceFile.isBuiltInStubFile(),
isThirdPartyImport,
isInPyTypedPackage,
effectiveIsInPyTypedPackage,
this._editModeTracker,
{
isTracked: true,
Expand Down
163 changes: 163 additions & 0 deletions packages/pyright-internal/src/tests/privateImportUsage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* 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, ExecutionEnvironment, getStandardDiagnosticRuleSet } from '../common/configOptions';
import { DiagnosticCategory } from '../common/diagnostic';
import { FullAccessHost } from '../common/fullAccessHost';
import { lib, sitePackages } from '../common/pathConsts';
import { combinePaths, getDirectoryPath, normalizeSlashes } from '../common/pathUtils';
import { createServiceProvider } from '../common/serviceProviderExtensions';
import { Uri } from '../common/uri/uri';
import { UriEx } from '../common/uri/uriUtils';
import { TestAccessHost } from './harness/testAccessHost';
import { TestFileSystem } from './harness/vfs/filesystem';
import { PyrightFileSystem } from '../pyrightFileSystem';

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 PyrightFileSystem(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();
});
});