Files
Viper_VSCode/packages/vipeye-internal/src/analyzer/sourceFile.ts
T
TermiNexus aef18c2891
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
更新了一些事项,feat(vipeye-internal): builtins.vpi 声明编译器内置 typedef rowptr = t.CPtr;feat(vipeye-internal): None 返回函数仅允许裸 return/return None,return 值为 None 的表达式报错;fix(vipeye-internal): builtins.vp 发现扩展到文件祖先目录与 includes 子目录,修复 uint64 等 typedef 未定义
2026-09-10 16:32:23 +08:00

1609 lines
66 KiB
TypeScript

/*
* sourceFile.ts
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
* Author: Eric Traut
*
* Class that represents a single Python source or stub file.
*/
import { isMainThread } from 'worker_threads';
import { OperationCanceledException } from '../common/cancellationUtils';
import { appendArray } from '../common/collectionUtils';
import {
ConfigOptions,
DiagnosticRuleSet,
ExecutionEnvironment,
getBasicDiagnosticRuleSet,
} from '../common/configOptions';
import { ConsoleInterface, StandardConsole } from '../common/console';
import { assert } from '../common/debug';
import { Diagnostic, DiagnosticCategory, TaskListToken, convertLevelToCategory } from '../common/diagnostic';
import { DiagnosticRule } from '../common/diagnosticRules';
import { DiagnosticSink, TextRangeDiagnosticSink } from '../common/diagnosticSink';
import { FileSystem } from '../common/fileSystem';
import { LogTracker, getPathForLogging } from '../common/logTracker';
import { stripFileExtension } from '../common/pathUtils';
import { convertOffsetsToRange, convertTextRangeToRange } from '../common/positionUtils';
import { ServiceKeys } from '../common/serviceKeys';
import { ServiceProvider } from '../common/serviceProvider';
import '../common/serviceProviderExtensions';
import * as StringUtils from '../common/stringUtils';
import { Range, TextRange, getEmptyRange } from '../common/textRange';
import { TextRangeCollection } from '../common/textRangeCollection';
import { Duration, timingStats } from '../common/timing';
import { Uri } from '../common/uri/uri';
import { LocMessage } from '../localization/localize';
import { ModuleNode } from '../parser/parseNodes';
import { ModuleImport, ParseFileResults, ParseOptions, Parser, ParserOutput } from '../parser/parser';
import { IgnoreComment, Tokenizer, TokenizerOutput } from '../parser/tokenizer';
import { Token } from '../parser/tokenizerTypes';
import { AnalyzerFileInfo, ImportLookup } from './analyzerFileInfo';
import * as AnalyzerNodeInfo from './analyzerNodeInfo';
import { Binder } from './binder';
import { CellChainIndexProvider } from './cellChainIndex';
import { Checker } from './checker';
import { CircularDependency } from './circularDependency';
import * as CommentUtils from './commentUtils';
import { ImportResolver } from './importResolver';
import { ImportResult } from './importResult';
import { ParseTreeCleanerWalker } from './parseTreeCleaner';
import { Scope } from './scope';
import { SymbolTable } from './symbol';
import { TestWalker } from './testWalker';
import { TypeEvaluator } from './typeEvaluatorTypes';
// Limit the number of import cycles tracked per source file.
const _maxImportCyclesPerFile = 4;
// Allow files up to 50MB in length, same as VS Code.
// https://github.com/microsoft/vscode/blob/1e750a7514f365585d8dab1a7a82e0938481ea2f/src/vs/editor/common/model/textModel.ts#L194
export const maxSourceFileSize = 50 * 1024 * 1024;
interface ResolveImportResult {
imports: ImportResult[];
builtinsImportResult?: ImportResult | undefined;
}
// Indicates whether IPython syntax is supported and if so, what
// type of notebook support is in use.
export enum IPythonMode {
// Not a notebook. This is the only falsy enum value, so you
// can test if IPython is supported via "if (ipythonMode)"
None = 0,
// Each cell is its own document.
CellDocs,
}
// A monotonically increasing number used to create unique file IDs.
let nextUniqueFileId = 1;
class WriteableData {
// Number that is incremented every time the diagnostics
// are updated.
diagnosticVersion = 0;
// Generation count of the file contents. When the contents
// change, this is incremented.
fileContentsVersion = 0;
// Number that is incremented every time semantic of the file
// might have changed.
semanticVersion = 0;
// Length and hash of the file the last time it was read from disk.
lastFileContentLength: number | undefined = undefined;
lastFileContentHash: number | undefined = undefined;
// Client's version of the file. Undefined implies that contents
// need to be read from disk.
clientDocumentContents: string | undefined;
clientDocumentVersion: number | undefined;
// Version of file contents that have been analyzed.
analyzedFileContentsVersion = -1;
// Do we need to walk the parse tree and clean
// the binder information hanging from it?
parseTreeNeedsCleaning = false;
parsedFileContents: string | undefined;
tokenizerLines: TextRangeCollection<TextRange> | undefined;
tokenizerOutput: TokenizerOutput | undefined;
lineCount: number | undefined;
moduleSymbolTable: SymbolTable | undefined;
// Reentrancy check for binding and checking.
isBindingInProgress = false;
isCheckingInProgress = false;
// Diagnostics generated during different phases of analysis.
parseDiagnostics: Diagnostic[] = [];
commentDiagnostics: Diagnostic[] = [];
bindDiagnostics: Diagnostic[] = [];
checkerDiagnostics: Diagnostic[] = [];
taskListDiagnostics: Diagnostic[] = [];
typeIgnoreLines = new Map<number, IgnoreComment>();
typeIgnoreAll: IgnoreComment | undefined;
viperIgnoreLines = new Map<number, IgnoreComment>();
// Accumulated and filtered diagnostics that combines all of the
// above information. This needs to be recomputed any time the
// above change.
accumulatedDiagnostics: Diagnostic[] = [];
diagnosticsWithoutFileIgnore: Diagnostic[] = [];
// Circular dependencies that have been reported in this file.
circularDependencies: CircularDependency[] = [];
noCircularDependencyConfirmed = false;
// Did we hit the maximum import depth?
hitMaxImportDepth: number | undefined;
// Do we need to perform a binding step?
isBindingNeeded = true;
// Do we have valid diagnostic results from a checking pass?
isCheckingNeeded = true;
// Time (in ms) that the last check() call required for this file.
checkTime: number | undefined;
// Information about implicit and explicit imports from this file.
imports: ImportResult[] | undefined;
builtinsImport: ImportResult | undefined;
// True if the file appears to have been deleted.
isFileDeleted = false;
parserOutput: ParserOutput | undefined;
constructor() {
// Empty
}
debugPrint() {
return `WritableData:
diagnosticVersion=${this.diagnosticVersion},
noCircularDependencyConfirmed=${this.noCircularDependencyConfirmed},
isBindingNeeded=${this.isBindingNeeded},
isBindingInProgress=${this.isBindingInProgress},
isCheckingInProgress=${this.isCheckingInProgress},
isCheckingNeeded=${this.isCheckingNeeded},
isFileDeleted=${this.isFileDeleted},
hitMaxImportDepth=${this.hitMaxImportDepth},
parseTreeNeedsCleaning=${this.parseTreeNeedsCleaning},
fileContentsVersion=${this.fileContentsVersion},
analyzedFileContentsVersion=${this.analyzedFileContentsVersion},
clientDocumentVersion=${this.clientDocumentVersion},
lastFileContentLength=${this.lastFileContentLength},
lastFileContentHash=${this.lastFileContentHash},
typeIgnoreAll=${this.typeIgnoreAll},
imports=${this.imports?.length},
builtinsImport=${this.builtinsImport?.importName},
circularDependencies=${this.circularDependencies?.length},
parseDiagnostics=${this.parseDiagnostics?.length},
commentDiagnostics=${this.commentDiagnostics?.length},
bindDiagnostics=${this.bindDiagnostics?.length},
checkerDiagnostics=${this.checkerDiagnostics?.length},
taskListDiagnostics=${this.taskListDiagnostics?.length},
accumulatedDiagnostics=${this.accumulatedDiagnostics?.length},
typeIgnoreLines=${this.typeIgnoreLines?.size},
viperIgnoreLines=${this.viperIgnoreLines?.size},
checkTime=${this.checkTime},
clientDocumentContents=${this.clientDocumentContents?.length},
parseResults=${this.parserOutput?.parseTree.length},
semanticVersion=${this.semanticVersion}, `;
}
}
export interface SourceFileEditMode {
readonly isEditMode: boolean;
}
export class SourceFile {
// Console interface to use for debugging.
private _console: ConsoleInterface;
// Uri unique to this file within the workspace. May not represent
// a real file on disk.
private readonly _uri: Uri;
// A short string that is guaranteed to uniquely
// identify this file.
private readonly _fileId: string;
// Getter to lazily compute the module name from the file URI.
private _moduleNameGetter: (file: Uri) => string;
// Period-delimited import path for the module.
private _cachedModuleName: string | undefined;
// True if file is a type-hint (.pyi) file versus a python
// (.py) file.
private readonly _isStubFile: boolean;
// True if the file was imported as a third-party import.
private readonly _isThirdPartyImport: boolean;
// True if the file is the "typing.pyi" file, which needs
// special-case handling.
private readonly _isTypingStubFile: boolean;
// True if the file is the "typing_extensions.pyi" file, which needs
// special-case handling.
private readonly _isTypingExtensionsStubFile: boolean;
// True if the file is the "_typeshed.pyi" file, which needs special-
// case handling.
private readonly _isTypeshedStubFile: boolean;
// True if the file one of the other built-in stub files
// that require special-case handling: "collections.pyi",
// "dataclasses.pyi", "abc.pyi", "asyncio/coroutines.pyi".
private readonly _isBuiltInStubFile: boolean;
// True if the file is part of a package that contains a
// "py.typed" file.
private readonly _isThirdPartyPyTypedPresent: boolean;
private readonly _editMode: SourceFileEditMode;
// Settings that control which diagnostics should be output. The rules
// are initialized to the basic set. They should be updated after the
// the file is parsed.
private _diagnosticRuleSet = getBasicDiagnosticRuleSet();
// Indicate whether this file is for ipython or not.
private _ipythonMode = IPythonMode.None;
private _logTracker: LogTracker;
private _preEditData: WriteableData | undefined;
// Data that changes when the source file changes.
private _writableData: WriteableData;
readonly fileSystem: FileSystem;
constructor(
readonly serviceProvider: ServiceProvider,
uri: Uri,
moduleNameGetter: (file: Uri) => string,
isThirdPartyImport: boolean,
isThirdPartyPyTypedPresent: boolean,
editMode: SourceFileEditMode,
console?: ConsoleInterface,
logTracker?: LogTracker,
ipythonMode?: IPythonMode
) {
this.fileSystem = serviceProvider.get(ServiceKeys.fs);
this._console = console || new StandardConsole();
this._writableData = new WriteableData();
this._editMode = editMode;
this._uri = uri;
this._fileId = this._makeFileId(uri);
this._moduleNameGetter = moduleNameGetter;
this._isStubFile = uri.hasExtension('.vpi') || uri.hasExtension('.vpnci');
this._isThirdPartyImport = isThirdPartyImport;
this._isThirdPartyPyTypedPresent = isThirdPartyPyTypedPresent;
const fileName = uri.fileName;
this._isTypingStubFile =
this._isStubFile && (this._uri.pathEndsWith('stdlib/typing.vpi') || fileName === 'typing_extensions.vpi');
this._isTypingExtensionsStubFile = this._isStubFile && fileName === 'typing_extensions.vpi';
this._isTypeshedStubFile =
this._isStubFile &&
(this._uri.pathEndsWith('stdlib/_typeshed/__init__.vpi') ||
this._uri.pathEndsWith('stdlib/_typeshed/_type_checker_internals.vpi'));
this._isBuiltInStubFile = false;
if (this._isStubFile) {
if (
this._uri.pathEndsWith('stdlib/collections/__init__.vpi') ||
this._uri.pathEndsWith('stdlib/asyncio/futures.vpi') ||
this._uri.pathEndsWith('stdlib/asyncio/tasks.vpi') ||
this._uri.pathEndsWith('stdlib/builtins.vpi') ||
this._uri.pathEndsWith('stdlib/_importlib_modulespec.vpi') ||
this._uri.pathEndsWith('stdlib/dataclasses.vpi') ||
this._uri.pathEndsWith('stdlib/abc.vpi') ||
this._uri.pathEndsWith('stdlib/enum.vpi') ||
this._uri.pathEndsWith('stdlib/queue.vpi') ||
this._uri.pathEndsWith('stdlib/string/templatelib.vpi') ||
this._uri.pathEndsWith('stdlib/types.vpi') ||
this._uri.pathEndsWith('stdlib/warnings.vpi')
) {
this._isBuiltInStubFile = true;
}
}
// 'FG' or 'BG' based on current thread.
this._logTracker = logTracker ?? new LogTracker(console, isMainThread ? 'FG' : 'BG');
this._ipythonMode = ipythonMode ?? IPythonMode.None;
}
// Sets the initial diagnostic rule set from the execution environment's
// config-level overrides. This should be called immediately after
// construction so the file has the correct rules before parse/bind.
setInitialDiagnosticRuleSet(ruleSet: DiagnosticRuleSet) {
this._diagnosticRuleSet = { ...ruleSet };
}
getIPythonMode(): IPythonMode {
return this._ipythonMode;
}
getUri(): Uri {
return this._uri;
}
getModuleName(): string {
if (!this._cachedModuleName) {
// Call the module name getter. If it returns '' (which can happen if the file is not part
// of the project), fall back to the file name.)
return this._moduleNameGetter(this._uri) || stripFileExtension(this._uri.fileName);
}
return this._cachedModuleName;
}
clearCachedModuleName() {
this._cachedModuleName = undefined;
}
getDiagnosticVersion(): number {
return this._writableData.diagnosticVersion;
}
getParseDiagnostics(): Diagnostic[] {
return this._writableData.parseDiagnostics;
}
isStubFile() {
return this._isStubFile;
}
isTypingStubFile() {
return this._isTypingStubFile;
}
isTypeshedStubFile() {
return this._isTypeshedStubFile;
}
isBuiltInStubFile() {
return this._isBuiltInStubFile;
}
isThirdPartyPyTypedPresent() {
return this._isThirdPartyPyTypedPresent;
}
// Returns a list of cached diagnostics from the latest analysis job.
// If the prevVersion is specified, the method returns undefined if
// the diagnostics haven't changed.
getDiagnostics(options: ConfigOptions, prevDiagnosticVersion?: number): Diagnostic[] | undefined {
if (this._writableData.diagnosticVersion === prevDiagnosticVersion) {
return undefined;
}
return this._writableData.accumulatedDiagnostics;
}
getDiagnosticsWithoutFileIgnore(): Diagnostic[] {
return this._writableData.diagnosticsWithoutFileIgnore;
}
getImports(): ImportResult[] {
return this._writableData.imports || [];
}
getBuiltinsImport(): ImportResult | undefined {
return this._writableData.builtinsImport;
}
getModuleSymbolTable(): SymbolTable | undefined {
return this._writableData.moduleSymbolTable;
}
getCheckTime() {
return this._writableData.checkTime;
}
restore(): string | undefined {
// If we had an edit, return our text.
if (this._preEditData) {
const text = this._writableData.clientDocumentContents!;
this._writableData = this._preEditData;
this._preEditData = undefined;
return text;
}
return undefined;
}
// Indicates whether the contents of the file have changed since
// the last analysis was performed.
didContentsChangeOnDisk(): boolean {
// If this is an open file any content changes will be
// provided through the editor. We can assume contents
// didn't change without us knowing about them.
if (this._writableData.clientDocumentContents) {
return false;
}
// If the file was never read previously we can't tell if the file has changed or not so
// we'll assume that it has. Otherwise, we may fail to analyze a file that was changed.
if (this._writableData.lastFileContentLength === undefined) {
return true;
}
// Read in the latest file contents and see if the hash matches
// that of the previous contents.
try {
// Read the file's contents.
if (this.fileSystem.existsSync(this._uri)) {
const fileContents = this.fileSystem.readFileSync(this._uri, 'utf8');
if (fileContents.length !== this._writableData.lastFileContentLength) {
return true;
}
if (StringUtils.hashString(fileContents) !== this._writableData.lastFileContentHash) {
return true;
}
} else {
// No longer exists, so yes it has changed.
return true;
}
} catch (error) {
return true;
}
return false;
}
// Drop parse and binding info to save memory. It is used
// in cases where memory is low. When info is needed, the file
// will be re-parsed and rebound.
dropParseAndBindInfo(): void {
// If we are actively binding or checking this file, we can't
// safely drop parse and binding info.
if (this._writableData.isBindingInProgress || this._writableData.isCheckingInProgress) {
return;
}
this._fireFileDirtyEvent();
this._writableData.parserOutput = undefined;
this._writableData.tokenizerLines = undefined;
this._writableData.tokenizerOutput = undefined;
this._writableData.parsedFileContents = undefined;
this._writableData.moduleSymbolTable = undefined;
this._writableData.isBindingNeeded = true;
this._writableData.imports = [];
}
markDirty(): void {
this._writableData.fileContentsVersion++;
this._writableData.semanticVersion++;
this._writableData.noCircularDependencyConfirmed = false;
this._writableData.isCheckingNeeded = true;
this._writableData.isBindingNeeded = true;
this._writableData.moduleSymbolTable = undefined;
this._writableData.lineCount = undefined;
this._fireFileDirtyEvent();
}
markReanalysisRequired(forceRebinding: boolean): void {
// Keep the parse info, but reset the analysis to the beginning.
this._writableData.semanticVersion++;
this._writableData.isCheckingNeeded = true;
this._writableData.noCircularDependencyConfirmed = false;
// If the file contains a wildcard import or __all__ symbols,
// we need to rebind because a dependent import may have changed.
if (this._writableData.parserOutput) {
if (
this._writableData.parserOutput.containsWildcardImport ||
AnalyzerNodeInfo.getDunderAllInfo(this._writableData.parserOutput.parseTree) !== undefined ||
forceRebinding
) {
// We don't need to rebuild index data since wildcard
// won't affect user file indices. User file indices
// don't contain import alias info.
this._writableData.parseTreeNeedsCleaning = true;
this._writableData.isBindingNeeded = true;
this._writableData.moduleSymbolTable = undefined;
}
}
}
getFileContentsVersion() {
return this._writableData.fileContentsVersion;
}
getClientVersion() {
return this._writableData.clientDocumentVersion;
}
getSemanticVersion() {
return this._writableData.semanticVersion;
}
getRange() {
return { start: { line: 0, character: 0 }, end: { line: this._writableData.lineCount ?? 0, character: 0 } };
}
getOpenFileContents() {
return this._writableData.clientDocumentContents;
}
getFileContent(): string | undefined {
// Get current buffer content if the file is opened.
const openFileContent = this.getOpenFileContents();
if (openFileContent !== undefined) {
return openFileContent;
}
// Ensure that the content used here is identical to the content obtained from the parse results.
if (!this.isParseRequired() && this._writableData.parsedFileContents !== undefined) {
return this._writableData.parsedFileContents;
}
// Otherwise, get content from file system.
try {
// Check the file's length before attempting to read its full contents.
const fileStat = this.fileSystem.statSync(this._uri);
if (fileStat.size > maxSourceFileSize) {
this._console.error(
`File length of "${this._uri}" is ${fileStat.size} ` +
`which exceeds the maximum supported file size of ${maxSourceFileSize}`
);
throw new Error('File larger than max');
}
return this.fileSystem.readFileSync(this._uri, 'utf8');
} catch (error) {
return undefined;
}
}
setClientVersion(version: number | null, contents: string): void {
// Save pre edit state if in edit mode.
this._cachePreEditState();
if (version === null) {
this._writableData.clientDocumentVersion = undefined;
this._writableData.clientDocumentContents = undefined;
// Since the file is no longer open, dump the tokenizer output
// so it doesn't consume memory.
this._writableData.tokenizerOutput = undefined;
} else {
this._writableData.clientDocumentVersion = version;
this._writableData.clientDocumentContents = contents;
const contentsHash = StringUtils.hashString(contents);
// Have the contents of the file changed?
if (
contents.length !== this._writableData.lastFileContentLength ||
contentsHash !== this._writableData.lastFileContentHash
) {
this.markDirty();
}
this._writableData.lastFileContentLength = contents.length;
this._writableData.lastFileContentHash = contentsHash;
this._writableData.isFileDeleted = false;
}
}
prepareForClose() {
this._fireFileDirtyEvent();
}
isFileDeleted() {
return this._writableData.isFileDeleted;
}
isParseRequired() {
return (
!this._writableData.parserOutput ||
this._writableData.analyzedFileContentsVersion !== this._writableData.fileContentsVersion
);
}
isBindingRequired() {
if (this._writableData.isBindingInProgress) {
return false;
}
if (this.isParseRequired()) {
return true;
}
return this._writableData.isBindingNeeded;
}
isCheckingRequired() {
return this._writableData.isCheckingNeeded;
}
getParseResults(): ParseFileResults | undefined {
if (this.isParseRequired()) {
return undefined;
}
assert(this._writableData.parserOutput !== undefined && this._writableData.parsedFileContents !== undefined);
// If we've cached the tokenizer output, use the cached version.
// Otherwise re-tokenize the contents on demand.
const tokenizeContents = this._tokenizeContents.bind(this);
const parsedFileContents = this._writableData.parsedFileContents;
const contentHash =
this._writableData.lastFileContentHash || StringUtils.hashString(this._writableData.parsedFileContents);
let tokenizerOutput: TokenizerOutput | undefined = this._writableData.tokenizerOutput;
return {
contentHash,
parserOutput: this._writableData.parserOutput,
get tokenizerOutput(): TokenizerOutput {
// Lazily tokenize the file contents only when accessed for the first time.
if (!tokenizerOutput) {
tokenizerOutput = tokenizeContents(parsedFileContents, contentHash);
}
return tokenizerOutput!;
},
text: this._writableData.parsedFileContents,
};
}
getParserOutput(): ParserOutput | undefined {
if (this.isParseRequired()) {
return undefined;
}
assert(this._writableData.parserOutput !== undefined);
return this._writableData.parserOutput;
}
// Adds a new circular dependency for this file but only if
// it hasn't already been added.
addCircularDependency(configOptions: ConfigOptions, circDependency: CircularDependency) {
let updatedDependencyList = false;
// Some topologies can result in a massive number of cycles. We'll cut it off.
if (this._writableData.circularDependencies.length < _maxImportCyclesPerFile) {
if (!this._writableData.circularDependencies.some((dep) => dep.isEqual(circDependency))) {
this._writableData.circularDependencies.push(circDependency);
updatedDependencyList = true;
}
}
if (updatedDependencyList) {
this._recomputeDiagnostics(configOptions);
}
}
setNoCircularDependencyConfirmed() {
this._writableData.noCircularDependencyConfirmed = true;
}
isNoCircularDependencyConfirmed() {
return !this.isParseRequired() && this._writableData.noCircularDependencyConfirmed;
}
setHitMaxImportDepth(maxImportDepth: number) {
this._writableData.hitMaxImportDepth = maxImportDepth;
}
// Parse the file and update the state. Callers should wait for completion
// (or at least cancel) prior to calling again. It returns true if a parse
// was required and false if the parse information was up to date already.
parse(configOptions: ConfigOptions, importResolver: ImportResolver, content?: string): boolean {
return this._logTracker.log(`parsing: ${this._getPathForLogging(this._uri)}`, (logState) => {
// If the file is already parsed, we can skip.
if (!this.isParseRequired()) {
logState.suppress();
return false;
}
const diagSink = this.createDiagnosticSink();
let fileContents = this.getOpenFileContents();
if (fileContents === undefined) {
try {
const startTime = timingStats.readFileTime.totalTime;
timingStats.readFileTime.timeOperation(() => {
// Read the file's contents.
fileContents = content ?? this.getFileContent();
if (fileContents === undefined) {
throw new Error("Can't get file content");
}
// Remember the length and hash for comparison purposes.
this._writableData.lastFileContentLength = fileContents.length;
this._writableData.lastFileContentHash = StringUtils.hashString(fileContents);
});
logState.add(`fs read ${timingStats.readFileTime.totalTime - startTime}ms`);
} catch (error) {
diagSink.addError(`Source file could not be read`, getEmptyRange());
fileContents = '';
if (!this.fileSystem.existsSync(this._uri)) {
this._writableData.isFileDeleted = true;
}
}
}
try {
// Parse the token stream, building the abstract syntax tree.
const parseFileResults = this._parseFile(
configOptions,
this._uri,
fileContents!,
this._ipythonMode !== IPythonMode.None,
diagSink
);
assert(parseFileResults !== undefined && parseFileResults.tokenizerOutput !== undefined);
this._writableData.parserOutput = parseFileResults.parserOutput;
this._writableData.tokenizerLines = parseFileResults.tokenizerOutput.lines;
this._writableData.parsedFileContents = fileContents;
this._writableData.typeIgnoreLines = parseFileResults.tokenizerOutput.typeIgnoreLines;
this._writableData.typeIgnoreAll = parseFileResults.tokenizerOutput.typeIgnoreAll;
this._writableData.viperIgnoreLines = parseFileResults.tokenizerOutput.viperIgnoreLines;
this._writableData.lineCount = parseFileResults.tokenizerOutput.lines.length;
// Cache the tokenizer output only if this file is open.
if (this._writableData.clientDocumentContents !== undefined) {
this._writableData.tokenizerOutput = parseFileResults.tokenizerOutput;
}
// Resolve imports.
const execEnvironment = configOptions.findExecEnvironment(this._uri);
timingStats.resolveImportsTime.timeOperation(() => {
const importResult = this._resolveImports(
importResolver,
parseFileResults.parserOutput.importedModules,
execEnvironment
);
this._writableData.imports = importResult.imports;
this._writableData.builtinsImport = importResult.builtinsImportResult;
this._writableData.parseDiagnostics = diagSink.fetchAndClear();
this._writableData.taskListDiagnostics = [];
this._addTaskListDiagnostics(
configOptions.taskListTokens,
parseFileResults,
this._writableData.taskListDiagnostics
);
});
// Is this file in a "strict" path?
const useStrict =
configOptions.strict.find((strictFileSpec) => this._uri.matchesRegex(strictFileSpec.regExp)) !==
undefined;
const commentDiags: CommentUtils.CommentDiagnostic[] = [];
this._diagnosticRuleSet = CommentUtils.getFileLevelDirectives(
parseFileResults.tokenizerOutput.tokens,
parseFileResults.tokenizerOutput.lines,
execEnvironment.diagnosticRuleSet,
useStrict,
commentDiags
);
this._writableData.commentDiagnostics = [];
commentDiags.forEach((commentDiag) => {
this._writableData.commentDiagnostics.push(
new Diagnostic(
DiagnosticCategory.Error,
commentDiag.message,
convertTextRangeToRange(commentDiag.range, parseFileResults.tokenizerOutput.lines)
)
);
});
} catch (e: any) {
const message: string =
(e.stack ? e.stack.toString() : undefined) ||
(typeof e.message === 'string' ? e.message : undefined) ||
JSON.stringify(e);
this._console.error(
LocMessage.internalParseError().format({
file: this.getUri().toUserVisibleString(),
message,
})
);
// Create dummy parse results.
this._writableData.parsedFileContents = '';
this._writableData.tokenizerLines = new TextRangeCollection<TextRange>([]);
this._writableData.parserOutput = {
parseTree: ModuleNode.create({ start: 0, length: 0 }),
importedModules: [],
futureImports: new Set<string>(),
containsWildcardImport: false,
typingSymbolAliases: new Map<string, string>(),
hasTypeAnnotations: false,
lines: this._writableData.tokenizerLines,
};
this._writableData.tokenizerOutput = {
tokens: new TextRangeCollection<Token>([]),
lines: this._writableData.tokenizerLines,
typeIgnoreAll: undefined,
typeIgnoreLines: new Map<number, IgnoreComment>(),
viperIgnoreLines: new Map<number, IgnoreComment>(),
predominantEndOfLineSequence: '\n',
hasPredominantTabSequence: false,
predominantTabSequence: ' ',
predominantSingleQuoteCharacter: "'",
};
this._writableData.imports = undefined;
this._writableData.builtinsImport = undefined;
const diagSink = this.createDiagnosticSink();
diagSink.addError(
LocMessage.internalParseError().format({
file: this.getUri().toUserVisibleString(),
message,
}),
getEmptyRange()
);
this._writableData.parseDiagnostics = diagSink.fetchAndClear();
this._writableData.taskListDiagnostics = diagSink.fetchAndClear();
// Do not rethrow the exception, swallow it here. Callers are not
// prepared to handle an exception.
}
this._writableData.analyzedFileContentsVersion = this._writableData.fileContentsVersion;
this._writableData.isBindingNeeded = true;
this._writableData.isCheckingNeeded = true;
this._writableData.parseTreeNeedsCleaning = false;
this._writableData.hitMaxImportDepth = undefined;
this._recomputeDiagnostics(configOptions);
return true;
});
}
bind(
configOptions: ConfigOptions,
importLookup: ImportLookup,
builtinsScope: Scope | undefined,
futureImports: Set<string>,
cellChainIndex: CellChainIndexProvider | undefined
) {
assert(!this.isParseRequired(), 'Bind called before parsing');
assert(this.isBindingRequired(), 'Bind called unnecessarily');
assert(!this._writableData.isBindingInProgress, 'Bind called while binding in progress');
assert(this._writableData.parserOutput !== undefined, 'Parse results not available');
return this._logTracker.log(`binding: ${this._getPathForLogging(this._uri)}`, () => {
try {
// Perform name binding.
timingStats.bindTime.timeOperation(() => {
this._cleanParseTreeIfRequired();
const fileInfo = this._buildFileInfo(configOptions, importLookup, builtinsScope, futureImports);
AnalyzerNodeInfo.setFileInfo(this._writableData.parserOutput!.parseTree, fileInfo);
const binder = new Binder(fileInfo, configOptions.indexGenerationMode, cellChainIndex);
this._writableData.isBindingInProgress = true;
binder.bindModule(this._writableData.parserOutput!.parseTree);
// If we're in "test mode" (used for unit testing), run an additional
// "test walker" over the parse tree to validate its internal consistency.
if (configOptions.internalTestMode) {
const testWalker = new TestWalker();
testWalker.walk(this._writableData.parserOutput!.parseTree);
}
this._writableData.bindDiagnostics = fileInfo.diagnosticSink.fetchAndClear();
const moduleScope = AnalyzerNodeInfo.getScope(this._writableData.parserOutput!.parseTree);
assert(moduleScope !== undefined, 'Module scope not returned by binder');
this._writableData.moduleSymbolTable = moduleScope!.symbolTable;
});
} catch (e: any) {
const message: string =
(e.stack ? e.stack.toString() : undefined) ||
(typeof e.message === 'string' ? e.message : undefined) ||
JSON.stringify(e);
this._console.error(
LocMessage.internalBindError().format({
file: this.getUri().toUserVisibleString(),
message,
})
);
const diagSink = this.createDiagnosticSink();
diagSink.addError(
LocMessage.internalBindError().format({
file: this.getUri().toUserVisibleString(),
message,
}),
getEmptyRange()
);
this._writableData.bindDiagnostics = diagSink.fetchAndClear();
// Do not rethrow the exception, swallow it here. Callers are not
// prepared to handle an exception.
} finally {
this._writableData.isBindingInProgress = false;
}
// Prepare for the next stage of the analysis.
this._writableData.isCheckingNeeded = true;
this._writableData.isBindingNeeded = false;
this._recomputeDiagnostics(configOptions);
});
}
check(
configOptions: ConfigOptions,
importLookup: ImportLookup,
importResolver: ImportResolver,
evaluator: TypeEvaluator,
dependentFiles?: ParserOutput[]
) {
assert(!this.isParseRequired(), `Check called before parsing: state=${this._writableData.debugPrint()}`);
assert(!this.isBindingRequired(), `Check called before binding: state=${this._writableData.debugPrint()}`);
assert(!this._writableData.isBindingInProgress, 'Check called while binding in progress');
assert(!this._writableData.isCheckingInProgress, 'Check called while checking in progress');
assert(this.isCheckingRequired(), 'Check called unnecessarily');
assert(this._writableData.parserOutput !== undefined, 'Parse results not available');
return this._logTracker.log(`checking: ${this._getPathForLogging(this._uri)}`, () => {
try {
timingStats.typeCheckerTime.timeOperation(() => {
const checkDuration = new Duration();
const checker = new Checker(
importResolver,
evaluator,
this._writableData.parserOutput!,
dependentFiles
);
this._writableData.isCheckingInProgress = true;
checker.check();
this._writableData.isCheckingNeeded = false;
const fileInfo = AnalyzerNodeInfo.getFileInfo(this._writableData.parserOutput!.parseTree)!;
this._writableData.checkerDiagnostics = fileInfo.diagnosticSink.fetchAndClear();
this._writableData.checkTime = checkDuration.getDurationInMilliseconds();
});
} catch (e: any) {
const isCancellation = OperationCanceledException.is(e);
if (!isCancellation) {
const message: string =
(e.stack ? e.stack.toString() : undefined) ||
(typeof e.message === 'string' ? e.message : undefined) ||
JSON.stringify(e);
this._console.error(
LocMessage.internalTypeCheckingError().format({
file: this.getUri().toUserVisibleString(),
message,
})
);
const diagSink = this.createDiagnosticSink();
diagSink.addError(
LocMessage.internalTypeCheckingError().format({
file: this.getUri().toUserVisibleString(),
message,
}),
getEmptyRange()
);
this._writableData.checkerDiagnostics = diagSink.fetchAndClear();
// Mark the file as complete so we don't get into an infinite loop.
this._writableData.isCheckingNeeded = false;
}
throw e;
} finally {
this._writableData.isCheckingInProgress = false;
// Clear any circular dependencies associated with this file.
// These will be detected by the program module and associated
// with the source file right before it is finalized.
this._writableData.circularDependencies = [];
this._recomputeDiagnostics(configOptions);
}
});
}
test_enableIPythonMode(enable: boolean) {
this._ipythonMode = enable ? IPythonMode.CellDocs : IPythonMode.None;
}
protected createDiagnosticSink(): DiagnosticSink {
return new DiagnosticSink();
}
protected createTextRangeDiagnosticSink(lines: TextRangeCollection<TextRange>): TextRangeDiagnosticSink {
return new TextRangeDiagnosticSink(lines);
}
// Creates a short string that can be used to uniquely identify
// this file from all other files. It is used in the type evaluator
// to distinguish between types that are defined in different files
// or scopes.
private _makeFileId(uri: Uri) {
const maxNameLength = 8;
// Use a small portion of the file name to help with debugging.
let fileName = uri.fileNameWithoutExtensions;
if (fileName.length > maxNameLength) {
fileName = fileName.substring(fileName.length - maxNameLength);
}
// Append a number to guarantee uniqueness.
const uniqueNumber = nextUniqueFileId++;
// Use a "/" to separate the two components, since this
// character will never appear in a file name.
return `${fileName}/${uniqueNumber.toString()}`;
}
// Computes an updated set of accumulated diagnostics for the file
// based on the partial diagnostics from various analysis stages.
private _recomputeDiagnostics(configOptions: ConfigOptions) {
this._writableData.diagnosticVersion++;
let includeWarningsAndErrors = true;
// If a file was imported as a third-party file, don't report
// any errors for it. The user can't fix them anyway.
if (this._isThirdPartyImport) {
includeWarningsAndErrors = false;
}
let diagList: Diagnostic[] = [];
appendArray(diagList, this._writableData.parseDiagnostics);
appendArray(diagList, this._writableData.commentDiagnostics);
appendArray(diagList, this._writableData.bindDiagnostics);
appendArray(diagList, this._writableData.checkerDiagnostics);
appendArray(diagList, this._writableData.taskListDiagnostics);
const prefilteredDiagList = diagList;
const typeIgnoreLinesClone = new Map(this._writableData.typeIgnoreLines);
const viperIgnoreLinesClone = new Map(this._writableData.viperIgnoreLines);
// Filter the diagnostics based on "type: ignore" lines.
if (this._diagnosticRuleSet.enableTypeIgnoreComments) {
if (this._writableData.typeIgnoreLines.size > 0) {
diagList = diagList.filter((d) => {
if (
d.category !== DiagnosticCategory.UnusedCode &&
d.category !== DiagnosticCategory.UnreachableCode &&
d.category !== DiagnosticCategory.Deprecated
) {
for (let line = d.range.start.line; line <= d.range.end.line; line++) {
if (this._writableData.typeIgnoreLines.has(line)) {
typeIgnoreLinesClone.delete(line);
return false;
}
}
}
return true;
});
}
}
// Filter the diagnostics based on "viper: ignore" lines.
if (this._writableData.viperIgnoreLines.size > 0) {
diagList = diagList.filter((d) => {
if (
d.category !== DiagnosticCategory.UnusedCode &&
d.category !== DiagnosticCategory.UnreachableCode &&
d.category !== DiagnosticCategory.Deprecated
) {
for (let line = d.range.start.line; line <= d.range.end.line; line++) {
const viperIgnoreComment = this._writableData.viperIgnoreLines.get(line);
if (viperIgnoreComment) {
if (!viperIgnoreComment.rulesList) {
viperIgnoreLinesClone.delete(line);
return false;
}
const diagRule = d.getRule();
if (!diagRule) {
// If there's no diagnostic rule, it won't match
// against a rules list.
return true;
}
// Did we find this rule in the list?
if (viperIgnoreComment.rulesList.find((rule) => rule.text === diagRule)) {
// Update the viperIgnoreLinesClone to remove this rule.
const oldClone = viperIgnoreLinesClone.get(line);
if (oldClone?.rulesList) {
const filteredRulesList = oldClone.rulesList.filter(
(rule) => rule.text !== diagRule
);
if (filteredRulesList.length === 0) {
viperIgnoreLinesClone.delete(line);
} else {
viperIgnoreLinesClone.set(line, {
range: oldClone.range,
rulesList: filteredRulesList,
});
}
}
return false;
}
return true;
}
}
}
return true;
});
}
const unnecessaryTypeIgnoreDiags: Diagnostic[] = [];
// Skip this step if type checking is needed. Otherwise we'll likely produce
// incorrect (false positive) reportUnnecessaryTypeIgnoreComment diagnostics
// until checking is performed on this file.
if (
this._diagnosticRuleSet.reportUnnecessaryTypeIgnoreComment !== 'none' &&
!this._writableData.isCheckingNeeded
) {
const diagCategory = convertLevelToCategory(this._diagnosticRuleSet.reportUnnecessaryTypeIgnoreComment);
const prefilteredErrorList = prefilteredDiagList.filter(
(diag) =>
diag.category === DiagnosticCategory.Error ||
diag.category === DiagnosticCategory.Warning ||
diag.category === DiagnosticCategory.Information
);
const isUnreachableCodeRange = (range: Range) => {
return prefilteredDiagList.find(
(diag) =>
diag.category === DiagnosticCategory.UnreachableCode &&
diag.range.start.line <= range.start.line &&
diag.range.end.line >= range.end.line
);
};
if (prefilteredErrorList.length === 0 && this._writableData.typeIgnoreAll !== undefined) {
const rangeStart = this._writableData.typeIgnoreAll.range.start;
const rangeEnd = rangeStart + this._writableData.typeIgnoreAll.range.length;
const range = convertOffsetsToRange(rangeStart, rangeEnd, this._writableData.tokenizerLines!);
if (!isUnreachableCodeRange(range) && this._diagnosticRuleSet.enableTypeIgnoreComments) {
const diag = new Diagnostic(diagCategory, LocMessage.unnecessaryTypeIgnore(), range);
diag.setRule(DiagnosticRule.reportUnnecessaryTypeIgnoreComment);
unnecessaryTypeIgnoreDiags.push(diag);
}
}
typeIgnoreLinesClone.forEach((ignoreComment) => {
if (this._writableData.tokenizerLines!) {
const rangeStart = ignoreComment.range.start;
const rangeEnd = rangeStart + ignoreComment.range.length;
const range = convertOffsetsToRange(rangeStart, rangeEnd, this._writableData.tokenizerLines!);
if (!isUnreachableCodeRange(range) && this._diagnosticRuleSet.enableTypeIgnoreComments) {
const diag = new Diagnostic(diagCategory, LocMessage.unnecessaryTypeIgnore(), range);
diag.setRule(DiagnosticRule.reportUnnecessaryTypeIgnoreComment);
unnecessaryTypeIgnoreDiags.push(diag);
}
}
});
viperIgnoreLinesClone.forEach((ignoreComment) => {
if (this._writableData.tokenizerLines!) {
if (!ignoreComment.rulesList) {
const rangeStart = ignoreComment.range.start;
const rangeEnd = rangeStart + ignoreComment.range.length;
const range = convertOffsetsToRange(rangeStart, rangeEnd, this._writableData.tokenizerLines!);
if (!isUnreachableCodeRange(range)) {
const diag = new Diagnostic(diagCategory, LocMessage.unnecessaryTypeIgnore(), range);
diag.setRule(DiagnosticRule.reportUnnecessaryTypeIgnoreComment);
unnecessaryTypeIgnoreDiags.push(diag);
}
} else {
ignoreComment.rulesList.forEach((unusedRule) => {
const rangeStart = unusedRule.range.start;
const rangeEnd = rangeStart + unusedRule.range.length;
const range = convertOffsetsToRange(
rangeStart,
rangeEnd,
this._writableData.tokenizerLines!
);
if (!isUnreachableCodeRange(range)) {
const diag = new Diagnostic(
diagCategory,
LocMessage.unnecessaryViperIgnoreRule().format({
name: unusedRule.text,
}),
range
);
diag.setRule(DiagnosticRule.reportUnnecessaryTypeIgnoreComment);
unnecessaryTypeIgnoreDiags.push(diag);
}
});
}
}
});
}
if (
this._diagnosticRuleSet.reportImportCycles !== 'none' &&
this._writableData.circularDependencies.length > 0
) {
const category = convertLevelToCategory(this._diagnosticRuleSet.reportImportCycles);
this._writableData.circularDependencies.forEach((cirDep) => {
const diag = new Diagnostic(
category,
LocMessage.importCycleDetected() +
'\n' +
cirDep
.getPaths()
.map((path) => ' ' + path.toUserVisibleString())
.join('\n'),
getEmptyRange()
);
diag.setRule(DiagnosticRule.reportImportCycles);
diagList.push(diag);
});
}
if (this._writableData.hitMaxImportDepth !== undefined) {
diagList.push(
new Diagnostic(
DiagnosticCategory.Error,
LocMessage.importDepthExceeded().format({ depth: this._writableData.hitMaxImportDepth }),
getEmptyRange()
)
);
}
// If there is a "type: ignore" comment at the top of the file, clear
// the diagnostic list of all error, warning, and information diagnostics.
if (this._diagnosticRuleSet.enableTypeIgnoreComments) {
if (this._writableData.typeIgnoreAll !== undefined) {
diagList = diagList.filter(
(diag) =>
diag.category !== DiagnosticCategory.Error &&
diag.category !== DiagnosticCategory.Warning &&
diag.category !== DiagnosticCategory.Information
);
}
}
// Now add in the "unnecessary type ignore" diagnostics.
diagList = diagList.concat(unnecessaryTypeIgnoreDiags);
// If we're not returning any diagnostics, filter out all of
// the errors and warnings, leaving only the unreachable code
// and deprecated diagnostics.
if (!includeWarningsAndErrors) {
diagList = diagList.filter(
(diag) =>
diag.category === DiagnosticCategory.UnusedCode ||
diag.category === DiagnosticCategory.UnreachableCode ||
diag.category === DiagnosticCategory.Deprecated
);
}
// Capture the fully-filtered diagnostics before any file-level ignore clears them, so
// consumers that need the unsuppressed list (e.g. ignored-file quick fixes) can access it.
// For non-ignored files this is the same array reference as accumulatedDiagnostics below.
this._writableData.diagnosticsWithoutFileIgnore = diagList;
// If the file is in the ignore list, clear the diagnostic list.
if (configOptions.ignore.find((ignoreFileSpec) => this._uri.matchesRegex(ignoreFileSpec.regExp))) {
diagList = [];
}
this._writableData.accumulatedDiagnostics = diagList;
}
private _cachePreEditState() {
// If this is our first write, then make a copy of the writable data.
if (!this._editMode.isEditMode || this._preEditData) {
return;
}
// Copy over the writable data.
this._preEditData = this._writableData;
// Recreate all the writable data from scratch.
this._writableData = new WriteableData();
}
// Get all task list diagnostics for the current file and add them
// to the specified diagnostic list.
private _addTaskListDiagnostics(
taskListTokens: TaskListToken[] | undefined,
parseFileResults: ParseFileResults,
diagList: Diagnostic[]
) {
if (!taskListTokens || taskListTokens.length === 0 || !diagList) {
return;
}
const tokenizerOutput = parseFileResults.tokenizerOutput;
const fileContents = parseFileResults.text;
for (let i = 0; i < tokenizerOutput.tokens.count; i++) {
const token = tokenizerOutput.tokens.getItemAt(i);
// If there are no comments, skip this token.
if (!token.comments || token.comments.length === 0) {
continue;
}
for (const comment of token.comments) {
for (const taskToken of taskListTokens) {
// Match: optional leading whitespace, then taskToken.text (case-insensitive),
// then either (whitespace to end) or (non-alphanumeric char).
const commentStart = comment.start;
const commentEnd = commentStart + comment.length;
const taskText = taskToken.text;
const taskLen = taskText.length;
// Skip leading whitespace within the source text range.
let pos = commentStart;
while (pos < commentEnd) {
const ch = fileContents.charCodeAt(pos);
if (ch === 0x20 || ch === 0x09 || ch === 0x0a || ch === 0x0d || ch === 0x0c || ch === 0x0b) {
pos++;
} else {
break;
}
}
// Check if the task token text matches (case-insensitive).
if (pos + taskLen > commentEnd) {
continue;
}
let matched = true;
for (let k = 0; k < taskLen; k++) {
const a = fileContents.charCodeAt(pos + k);
const b = taskText.charCodeAt(k);
if (a !== b && (a | 0x20) !== (b | 0x20)) {
matched = false;
break;
}
}
if (!matched) {
continue;
}
// After the token, require whitespace-to-end or a non-word character.
const afterPos = pos + taskLen;
if (afterPos < commentEnd) {
const ch = fileContents.charCodeAt(afterPos);
// Check if ch is a word character [a-zA-Z0-9_]
const isWord =
(ch >= 0x61 && ch <= 0x7a) ||
(ch >= 0x41 && ch <= 0x5a) ||
(ch >= 0x30 && ch <= 0x39) ||
ch === 0x5f;
if (isWord) {
continue;
}
}
// Match succeeded. pos is the offset of the task token in the source text.
const rangeEnd = TextRange.getEnd(comment);
const range = convertOffsetsToRange(pos, rangeEnd, tokenizerOutput.lines!);
const commentValue = comment.value;
diagList.push(
new Diagnostic(DiagnosticCategory.TaskItem, commentValue.trim(), range, taskToken.priority)
);
}
}
}
}
private _buildFileInfo(
configOptions: ConfigOptions,
importLookup: ImportLookup,
builtinsScope: Scope | undefined,
futureImports: Set<string>
) {
assert(this._writableData.parserOutput !== undefined, 'Parse results not available');
const analysisDiagnostics = this.createTextRangeDiagnosticSink(this._writableData.tokenizerLines!);
const fileInfo: AnalyzerFileInfo = {
importLookup,
futureImports,
builtinsScope,
diagnosticSink: analysisDiagnostics,
executionEnvironment: configOptions.findExecEnvironment(this._uri),
diagnosticRuleSet: this._diagnosticRuleSet,
lines: this._writableData.tokenizerLines!,
typingSymbolAliases: this._writableData.parserOutput!.typingSymbolAliases,
definedConstants: configOptions.defineConstant,
fileId: this._fileId,
fileUri: this._uri,
moduleName: this.getModuleName(),
isStubFile: this._isStubFile,
isTypingStubFile: this._isTypingStubFile,
isTypingExtensionsStubFile: this._isTypingExtensionsStubFile,
isTypeshedStubFile: this._isTypeshedStubFile,
isBuiltInStubFile: this._isBuiltInStubFile,
isInPyTypedPackage: this._isThirdPartyPyTypedPresent,
ipythonMode: this._ipythonMode,
accessedSymbolSet: new Set<number>(),
};
return fileInfo;
}
private _cleanParseTreeIfRequired() {
if (this._writableData.parserOutput) {
if (this._writableData.parseTreeNeedsCleaning) {
const cleanerWalker = new ParseTreeCleanerWalker(this._writableData.parserOutput.parseTree);
cleanerWalker.clean();
this._writableData.parseTreeNeedsCleaning = false;
}
}
}
private _resolveImports(
importResolver: ImportResolver,
moduleImports: ModuleImport[],
execEnv: ExecutionEnvironment
): ResolveImportResult {
const imports: ImportResult[] = [];
const resolveAndAddIfNotSelf = (nameParts: string[], skipMissingImport = false) => {
const importResult = importResolver.resolveImport(this._uri, execEnv, {
leadingDots: 0,
nameParts,
importedSymbols: undefined,
});
if (skipMissingImport && !importResult.isImportFound) {
return undefined;
}
// Avoid importing module from the module file itself.
if (importResult.resolvedUris.length === 0 || importResult.resolvedUris[0] !== this._uri) {
imports.push(importResult);
return importResult;
}
return undefined;
};
// Always include an implicit import of the builtins module.
let builtinsImportResult: ImportResult | undefined;
// If this is a project source file (not a stub), try to resolve
// the __builtins__ stub first.
if (!this._isThirdPartyImport && !this._isStubFile) {
builtinsImportResult = resolveAndAddIfNotSelf(['__builtins__'], /* skipMissingImport */ true);
}
if (!builtinsImportResult) {
builtinsImportResult = resolveAndAddIfNotSelf(['builtins']);
}
resolveAndAddIfNotSelf(['_typeshed', '_type_checker_internals'], /* skipMissingImport */ true);
for (const moduleImport of moduleImports) {
const importResult = importResolver.resolveImport(this._uri, execEnv, {
leadingDots: moduleImport.leadingDots,
nameParts: moduleImport.nameParts,
importedSymbols: moduleImport.importedSymbols,
});
imports.push(importResult);
// Associate the import results with the module import
// name node in the parse tree so we can access it later
// (for hover and definition support).
if (moduleImport.nameParts.length === moduleImport.nameNode.d.nameParts.length) {
AnalyzerNodeInfo.setImportInfo(moduleImport.nameNode, importResult);
} else {
// For implicit imports of higher-level modules within a multi-part
// module name, the moduleImport.nameParts will refer to the subset
// of the multi-part name rather than the full multi-part name. In this
// case, store the import info on the name part node.
assert(moduleImport.nameParts.length > 0);
assert(moduleImport.nameParts.length - 1 < moduleImport.nameNode.d.nameParts.length);
AnalyzerNodeInfo.setImportInfo(
moduleImport.nameNode.d.nameParts[moduleImport.nameParts.length - 1],
importResult
);
}
}
return {
imports,
builtinsImportResult,
};
}
private _getPathForLogging(fileUri: Uri) {
return getPathForLogging(this.fileSystem, fileUri);
}
private _parseFile(
configOptions: ConfigOptions,
fileUri: Uri,
fileContents: string,
useNotebookMode: boolean,
diagSink: DiagnosticSink
): ParseFileResults {
// Use the configuration options to determine the environment zin which
// this source file will be executed.
const execEnvironment = configOptions.findExecEnvironment(fileUri);
const parseOptions = new ParseOptions();
parseOptions.useNotebookMode = useNotebookMode;
if (fileUri.pathEndsWith('vpi')) {
parseOptions.isStubFile = true;
}
parseOptions.pythonVersion = execEnvironment.pythonVersion;
parseOptions.dialect = execEnvironment.dialect;
parseOptions.skipFunctionAndClassBody = configOptions.indexGenerationMode ?? false;
// Parse the token stream, building the abstract syntax tree.
const parser = new Parser();
return parser.parseSourceFile(fileContents, parseOptions, diagSink);
}
private _tokenizeContents(fileContents: string, contentHash: number): TokenizerOutput {
const tokenizer = new Tokenizer();
const output = tokenizer.tokenize(fileContents);
// When the file is open, cache the tokenizer results.
// Because the tokenizer is lazy, ensure that the state remains unchanged before caching its output.
if (
this._writableData.clientDocumentContents !== undefined &&
this._writableData.lastFileContentHash === contentHash
) {
this._writableData.tokenizerOutput = output;
// Replace the existing tokenizerLines with the newly-returned
// version. They should have the same contents, but we want to use
// the same object so the older object can be deallocated.
this._writableData.tokenizerLines = output.lines;
}
return output;
}
private _fireFileDirtyEvent() {
this.serviceProvider.tryGet(ServiceKeys.stateMutationListeners)?.forEach((l) => {
try {
l.onFileDirty?.(this._uri);
} catch (ex: any) {
const console = this.serviceProvider.tryGet(ServiceKeys.console);
if (console) {
console.error(`State mutation listener exception: ${ex.message}`);
}
}
});
}
}