Skip to content

fix: Avoid crash in the presence of decorators. #184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 15, 2025
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,8 @@ function getTypeCategoryString(typeCategory: TypeCategory, type: any) {
}
}

class TreeDumper extends ParseTreeWalker {
// NOTE(scip-python): Exported for use in scip-python debugging
export class TreeDumper extends ParseTreeWalker {
private _indentation = '';
private _output = '';

Expand Down
21 changes: 21 additions & 0 deletions packages/pyright-scip/snapshots/input/graph_1278/mwe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import TypeVar, Generic, Callable, Iterator, ParamSpec
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test cases seems a bit large, but I haven't been able to minimize it further.


_T_co = TypeVar("_T_co")
_P = ParamSpec("_P")

class X(Generic[_T_co]):
pass

def decorate(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, X[_T_co]]: ...

class Foo:
@decorate
def foo(self) -> Iterator[None]: ...

@decorate
def noop():
yield

class FooImpl(Foo):
def foo(self):
return noop()
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ class A:
def x(self) -> int:
raise NotImplemented

def unmatched(self, x: int):
def matched_despite_different_type(self, x: int):
pass

class B(A):
def x(self) -> int:
return 5

def unmatched(self, x: int, y: int):
def matched_despite_different_type(self, x: int, y: int):
pass

def unrelated(self):
Expand Down
61 changes: 61 additions & 0 deletions packages/pyright-scip/snapshots/output/graph_1278/mwe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# < definition scip-python python snapshot-util 0.1 mwe/__init__:

from typing import TypeVar, Generic, Callable, Iterator, ParamSpec
# ^^^^^^ reference python-stdlib 3.11 typing/__init__:
# ^^^^^^^ reference python-stdlib 3.11 typing/TypeVar#
# ^^^^^^^ reference python-stdlib 3.11 typing/Generic.
# ^^^^^^^^ reference python-stdlib 3.11 typing/Callable.
# ^^^^^^^^ reference python-stdlib 3.11 typing/Iterator#
# ^^^^^^^^^ reference python-stdlib 3.11 typing/ParamSpec#

_T_co = TypeVar("_T_co")
#^^^^ definition snapshot-util 0.1 mwe/_T_co.
# ^^^^^^^ reference python-stdlib 3.11 typing/TypeVar#
_P = ParamSpec("_P")
#^ definition snapshot-util 0.1 mwe/_P.
# ^^^^^^^^^ reference python-stdlib 3.11 typing/ParamSpec#

class X(Generic[_T_co]):
# ^ definition snapshot-util 0.1 mwe/X#
# relationship implementation scip-python python python-stdlib 3.11 typing/Generic#
# ^^^^^^^ reference python-stdlib 3.11 typing/Generic.
# ^^^^^ reference snapshot-util 0.1 mwe/_T_co.
pass

def decorate(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, X[_T_co]]: ...
# ^^^^^^^^ definition snapshot-util 0.1 mwe/decorate().
# ^^^^ definition snapshot-util 0.1 mwe/decorate().(func)
# ^^^^^^^^ reference python-stdlib 3.11 typing/Callable.
# ^^ reference snapshot-util 0.1 mwe/_P.
# ^^^^^^^^ reference python-stdlib 3.11 typing/Iterator#
# ^^^^^ reference snapshot-util 0.1 mwe/_T_co.
# ^^^^^^^^ reference python-stdlib 3.11 typing/Callable.
# ^^ reference snapshot-util 0.1 mwe/_P.
# ^ reference snapshot-util 0.1 mwe/X#
# ^^^^^ reference snapshot-util 0.1 mwe/_T_co.

class Foo:
# ^^^ definition snapshot-util 0.1 mwe/Foo#
@decorate
# ^^^^^^^^ reference snapshot-util 0.1 mwe/decorate().
def foo(self) -> Iterator[None]: ...
# ^^^ definition snapshot-util 0.1 mwe/Foo#foo().
# ^^^^ definition snapshot-util 0.1 mwe/Foo#foo().(self)
# ^^^^^^^^ reference python-stdlib 3.11 typing/Iterator#

@decorate
#^^^^^^^^ reference snapshot-util 0.1 mwe/decorate().
def noop():
# ^^^^ definition snapshot-util 0.1 mwe/noop().
yield

class FooImpl(Foo):
# ^^^^^^^ definition snapshot-util 0.1 mwe/FooImpl#
# relationship implementation scip-python python snapshot-util 0.1 mwe/Foo#
# ^^^ reference snapshot-util 0.1 mwe/Foo#
def foo(self):
# ^^^ definition snapshot-util 0.1 mwe/FooImpl#foo().
# relationship implementation scip-python python snapshot-util 0.1 mwe/Foo#foo().
# ^^^^ definition snapshot-util 0.1 mwe/FooImpl#foo().(self)
return noop()
# ^^^^ reference snapshot-util 0.1 mwe/noop().
25 changes: 13 additions & 12 deletions packages/pyright-scip/snapshots/output/unique/inherits_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ def x(self) -> int:
raise NotImplemented
# ^^^^^^^^^^^^^^ reference python-stdlib 3.11 builtins/NotImplemented#

def unmatched(self, x: int):
# ^^^^^^^^^ definition snapshot-util 0.1 inherits_class/A#unmatched().
# ^^^^ definition snapshot-util 0.1 inherits_class/A#unmatched().(self)
# ^ definition snapshot-util 0.1 inherits_class/A#unmatched().(x)
# ^^^ reference python-stdlib 3.11 builtins/int#
def matched_despite_different_type(self, x: int):
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ definition snapshot-util 0.1 inherits_class/A#matched_despite_different_type().
# ^^^^ definition snapshot-util 0.1 inherits_class/A#matched_despite_different_type().(self)
# ^ definition snapshot-util 0.1 inherits_class/A#matched_despite_different_type().(x)
# ^^^ reference python-stdlib 3.11 builtins/int#
pass

class B(A):
Expand All @@ -27,13 +27,14 @@ def x(self) -> int:
# ^^^ reference python-stdlib 3.11 builtins/int#
return 5

def unmatched(self, x: int, y: int):
# ^^^^^^^^^ definition snapshot-util 0.1 inherits_class/B#unmatched().
# ^^^^ definition snapshot-util 0.1 inherits_class/B#unmatched().(self)
# ^ definition snapshot-util 0.1 inherits_class/B#unmatched().(x)
# ^^^ reference python-stdlib 3.11 builtins/int#
# ^ definition snapshot-util 0.1 inherits_class/B#unmatched().(y)
# ^^^ reference python-stdlib 3.11 builtins/int#
def matched_despite_different_type(self, x: int, y: int):
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ definition snapshot-util 0.1 inherits_class/B#matched_despite_different_type().
# relationship implementation scip-python python snapshot-util 0.1 inherits_class/A#matched_despite_different_type().
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This relationship is new. Python's overriding is based purely on name, not on types, so this is more accurate if we're looking at the runtime behavior.

# ^^^^ definition snapshot-util 0.1 inherits_class/B#matched_despite_different_type().(self)
# ^ definition snapshot-util 0.1 inherits_class/B#matched_despite_different_type().(x)
# ^^^ reference python-stdlib 3.11 builtins/int#
# ^ definition snapshot-util 0.1 inherits_class/B#matched_despite_different_type().(y)
# ^^^ reference python-stdlib 3.11 builtins/int#
pass

def unrelated(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ def three(self):
def shared(self) -> bool:
# ^^^^^^ definition snapshot-util 0.1 multiinherits_test/Multi#shared().
# relationship implementation scip-python python snapshot-util 0.1 multiinherits_test/Left#shared().
# relationship implementation scip-python python snapshot-util 0.1 multiinherits_test/Right#shared().
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This relationship is done, because based on MRO, only one of the parent class methods will be considered for overriding.

# ^^^^ definition snapshot-util 0.1 multiinherits_test/Multi#shared().(self)
# ^^^^ reference python-stdlib 3.11 builtins/bool#
return True
Expand Down
137 changes: 63 additions & 74 deletions packages/pyright-scip/src/treeVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { TypeEvaluator } from 'pyright-internal/analyzer/typeEvaluatorTypes';
import { convertOffsetToPosition } from 'pyright-internal/common/positionUtils';
import { TextRange } from 'pyright-internal/common/textRange';
import { TextRangeCollection } from 'pyright-internal/common/textRangeCollection';
import { printParseNodeType } from 'pyright-internal/analyzer/parseTreeUtils';
import { TreeDumper } from 'pyright-internal/commands/dumpFileDebugInfoCommand';
import {
AssignmentNode,
ClassNode,
Expand Down Expand Up @@ -38,7 +40,7 @@ import { SourceFile } from 'pyright-internal/analyzer/sourceFile';
import { extractParameterDocumentation } from 'pyright-internal/analyzer/docStringUtils';
import {
Declaration,
DeclarationType,
DeclarationType, FunctionDeclaration,
isAliasDeclaration,
isIntrinsicDeclaration,
} from 'pyright-internal/analyzer/declaration';
Expand All @@ -53,7 +55,7 @@ import { Event } from 'vscode-languageserver';
import { HoverResults } from 'pyright-internal/languageService/hoverProvider';
import { convertDocStringToMarkdown } from 'pyright-internal/analyzer/docStringConversion';
import { assert } from 'pyright-internal/common/debug';
import { getClassFieldsRecursive } from 'pyright-internal/analyzer/typeUtils';
import { ClassMemberLookupFlags, lookUpClassMember } from 'pyright-internal/analyzer/typeUtils';
import { PyrightFileSystem } from "pyright-internal/pyrightFileSystem";
import { createFromRealFileSystem } from "pyright-internal/common/realFileSystem";
import { normalizePathCase } from "pyright-internal/common/pathUtils";
Expand Down Expand Up @@ -218,6 +220,8 @@ export class TreeVisitor extends ParseTreeWalker {
const fileInfo = getFileInfo(node);
this.fileInfo = fileInfo;

// Pro tip: Use this.debugDumpAST(node, fileInfo) to see AST for debugging

// Insert definition at the top of the file
const pythonPackage = this.getPackageInfo(node, fileInfo.moduleName);
if (pythonPackage) {
Expand Down Expand Up @@ -342,54 +346,26 @@ export class TreeVisitor extends ParseTreeWalker {
let relationshipMap: Map<string, scip.Relationship> = new Map();
let classType = enclosingClassType.classType;

// Use: getClassMemberIterator
// Could use this to handle each of the fields with the same name
// but it's a bit weird if you have A -> B -> C, and then you say
// that C implements A's & B's... that seems perhaps a bit too verbose.
//
// See: https://github.com/sourcegraph/scip-python/issues/50
for (const base of classType.details.baseClasses) {
if (base.category !== TypeCategory.Class) {
continue;
}

let parentMethod = base.details.fields.get(node.name.value);
if (!parentMethod) {
let fieldLookup = getClassFieldsRecursive(base).get(node.name.value);
if (fieldLookup && fieldLookup.classType.category !== TypeCategory.Unknown) {
parentMethod = fieldLookup.classType.details.fields.get(node.name.value)!;
} else {
continue;
}
}
let classMember = lookUpClassMember(classType, node.name.value, ClassMemberLookupFlags.SkipOriginalClass);
if (!classMember) {
return undefined;
}

let parentMethodType = this.evaluator.getEffectiveTypeOfSymbol(parentMethod);
if (parentMethodType.category !== TypeCategory.Function) {
const superDecls = classMember.symbol.getDeclarations();
for (const superDecl of superDecls) {
if (superDecl.type !== DeclarationType.Function) {
continue;
}

if (
!ModifiedTypeUtils.isTypeImplementable(
functionType.functionType,
parentMethodType,
false,
true,
0,
true
)
) {
continue;
let symbol = this.getFunctionSymbol(superDecl);
if (!symbol.isLocal()) {
relationshipMap.set(
symbol.value,
new scip.Relationship({
symbol: symbol.value,
is_implementation: true,
})
);
}

let decl = parentMethodType.details.declaration!;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ! triggered the error reported in GRAPH-1278.

let symbol = this.typeToSymbol(decl.node.name, decl.node, parentMethodType);
relationshipMap.set(
symbol.value,
new scip.Relationship({
symbol: symbol.value,
is_implementation: true,
})
);
}

let relationships = Array.from(relationshipMap.values());
Expand Down Expand Up @@ -515,7 +491,6 @@ export class TreeVisitor extends ParseTreeWalker {
// (aliased_import and nested_items tests). So do both checks.
const resolvedPath = path.resolve(importInfo.resolvedPaths[0])
assertSometimesNormalized(resolvedPath, 'visitImportAs.resolvedPath')

return resolvedPath.startsWith(this.cwd) ||
resolvedPath.startsWith(
normalizePathCase(new PyrightFileSystem(createFromRealFileSystem()), this.cwd))
Expand Down Expand Up @@ -1320,44 +1295,49 @@ export class TreeVisitor extends ParseTreeWalker {
}
}

private getFunctionSymbol(decl: FunctionDeclaration): ScipSymbol {
const declModuleName = decl.moduleName;
let pythonPackage = this.guessPackage(declModuleName, decl.path);
if (!pythonPackage) {
return ScipSymbol.local(this.counter.next());
}

const enclosingClass = ParseTreeUtils.getEnclosingClass(decl.node);
if (enclosingClass) {
const enclosingClassType = this.evaluator.getTypeOfClass(enclosingClass);
if (enclosingClassType) {
let classType = enclosingClassType.classType;
const pythonPackage = this.guessPackage(classType.details.moduleName, classType.details.filePath)!;
const symbol = Symbols.makeClass(pythonPackage, classType.details.moduleName, classType.details.name);
return Symbols.makeMethod(symbol, decl.node.name.value);
}
return ScipSymbol.local(this.counter.next());
} else {
return Symbols.makeMethod(Symbols.makeModule(pythonPackage, declModuleName), decl.node.name.value);
}
}

// NOTE(tech-debt): typeToSymbol's signature doesn't make sense. It returns the
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// symbol for a _function_ (not the function's _type_) despite the name being
// 'typeToSymbol'. More generally, we should have dedicated functions to get
// the symbol based on the specific declarations, like getFunctionSymbol.
// There can be a general function which gets the symbol for a variety of kinds
// of _declarations_, but it must not take a _Type_ as an argument
// (Python mostly doesn't use structural types, so ~only declarations should
// have symbols).

// Take a `Type` from pyright and turn that into an LSIF symbol.
private typeToSymbol(node: NameNode, declNode: ParseNode, typeObj: Type): ScipSymbol {
if (Types.isFunction(typeObj)) {
// TODO: Possibly worth checking for parent declarations.
// I'm not sure if that will actually work though for types.

const decl = typeObj.details.declaration;
if (!decl) {
// throw 'Unhandled missing declaration for type: function';
// console.warn('Missing Function Decl:', node.token.value, typeObj);
return ScipSymbol.local(this.counter.next());
}

const declModuleName = decl.moduleName;
let pythonPackage = this.guessPackage(declModuleName, decl.path);
if (!pythonPackage) {
return ScipSymbol.local(this.counter.next());
}

const enclosingClass = ParseTreeUtils.getEnclosingClass(declNode);
if (enclosingClass) {
const enclosingClassType = this.evaluator.getTypeOfClass(enclosingClass);
if (enclosingClassType) {
let classType = enclosingClassType.classType;
const pythonPackage = this.guessPackage(classType.details.moduleName, classType.details.filePath)!;
const symbol = Symbols.makeClass(
pythonPackage,
classType.details.moduleName,
classType.details.name
);

return Symbols.makeMethod(symbol, node.value);
}

return ScipSymbol.local(this.counter.next());
} else {
return Symbols.makeMethod(Symbols.makeModule(pythonPackage, typeObj.details.moduleName), node.value);
}
return this.getFunctionSymbol(decl);
} else if (Types.isClass(typeObj)) {
const pythonPackage = this.getPackageInfo(node, typeObj.details.moduleName)!;
return Symbols.makeClass(pythonPackage, typeObj.details.moduleName, node.value);
Expand Down Expand Up @@ -1700,6 +1680,15 @@ export class TreeVisitor extends ParseTreeWalker {
this.rawSetLsifSymbol(node, symbol, symbol.isLocal());
return symbol;
}

private debugDumpAST(node: ModuleNode, fileInfo: AnalyzerFileInfo): void {
console.log("\n=== AST DUMP ===");
const dumper = new TreeDumper("", fileInfo.lines);
dumper.walk(node);
console.log(dumper.output);
console.log("=== END AST DUMP ===\n");
}

}

function _formatModuleName(node: ModuleNameNode): string {
Expand Down
Loading