Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
da80481
Fixed type annotations in assignments not creating symbols
ofeki-neosec Nov 1, 2024
f83a781
WIP
ofeki-neosec May 23, 2025
b362d65
Fixed class fields
ofeki-neosec May 24, 2025
b845777
Fixed module attributes occurrences appearing twice in symbol ID
ofeki-neosec May 29, 2025
3f976af
Fixed fields labels as builtins
ofeki-neosec May 29, 2025
e4c9139
Fixed constructors not getting occurrences
ofeki-neosec May 30, 2025
f814cd6
Fixed bug that caused missing class symbols
ofeki-neosec May 30, 2025
49be62a
More type assumptions
ofeki-neosec Jun 2, 2025
710367a
Fixed missing ctor occurrences
ofeki-neosec Jun 2, 2025
9d3653a
Fixed bug with const references marked as something else
ofeki-neosec Jun 2, 2025
f2db6fe
Small bugfixes
ofeki-neosec Jun 2, 2025
2d1c60b
Added workaround unknown packages
ofeki-neosec Oct 14, 2025
de0571a
Applied commit 8b60bbce1f2a4c7a517776cb395bbafb2e731e4f
ofeki-neosec Oct 14, 2025
476fae4
Added index.py
ofeki-neosec Oct 14, 2025
9bffde3
fix: Avoid crash in the presence of decorators. (#184)
varungandhi-src Aug 15, 2025
968dc4c
Fixed cherry-pick
ofeki-neosec Oct 14, 2025
25926de
More fixes
ofeki-neosec Dec 3, 2025
a0f236a
Support non-utf-8 encoding
ofeki-neosec Dec 4, 2025
0905fc2
Fixed issue with assertions failing script
ofeki-neosec Dec 22, 2025
c3fba03
Use canonical symbols for annotated variable information
chrapkowski-sg Nov 2, 2026
7df0420
test: resolve temporary fixture paths on macOS
chrapkowski-sg Nov 2, 2026
f39a41e
Merge current scip and preserve both regression suites
chrapkowski-sg Nov 2, 2026
ee092c7
Merge src-layout fix and retain all regression tests
chrapkowski-sg Nov 2, 2026
d296601
Merge current scip and align annotation regression snapshots
chrapkowski-sg Nov 2, 2026
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 @@ -2,7 +2,7 @@

from __future__ import annotations
# ^^^^^^^^^^ reference python-stdlib 3.11 __future__/__init__:
# ^^^^^^^^^^^ reference python-stdlib 3.11 __future__/annotations.annotations.
# ^^^^^^^^^^^ reference python-stdlib 3.11 __future__/annotations.

from contracts import Foo
# ^^^^^^^^^ reference scip-final-repro 0.1.0 contracts/__init__:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
# > #defines of CO\_xxx flags in Include/cpy...
# >
# > No feature line is ever to be deleted fr...
# ^^^^^^^^^^^ reference python-stdlib 3.11 __future__/annotations.annotations.
# ^^^^^^^^^^^ reference python-stdlib 3.11 __future__/annotations.
# external documentation ```python
# > (variable) annotations: _Feature
# > ```
Expand Down
46 changes: 12 additions & 34 deletions packages/pyright-scip/src/treeVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,28 +266,12 @@ export class TreeVisitor extends ParseTreeWalker {
}

override visitTypeAnnotation(node: TypeAnnotationNode): boolean {
// We are close to being able to look up a symbol, which could give us additional information here.
// Perhaps we should be using this for additional information for any given name?
// We can revisit this in visitName or perhaps when looking up the lsif symbol

// If we see a type annotation and we are currently inside of a class,
// that means that we are describing fields of a class (as far as I can tell),
// so we need to push a new symbol
const enclosingClass = ParseTreeUtils.getEnclosingClass(node, true);
if (enclosingClass) {
const hoverResult = this.program.getHoverForPosition(
this.fileInfo!.filePath,
convertOffsetToPosition(node.start, this.fileInfo!.lines),
'markdown',
_cancellationToken
);

this.document.symbols.push(
new scip.SymbolInformation({
symbol: this.getScipSymbol(node).value,
documentation: _formatHover(hoverResult!),
})
);
if (node.valueExpression.nodeType === ParseNodeType.Name) {
const name = node.valueExpression;
const declaration = this.evaluator.getDeclarationsForNameNode(name)?.[0];
if (declaration?.node) {
this.emitSymbolInformationOnce(name, this.getScipSymbol(declaration.node));
}
}

return true;
Expand All @@ -311,13 +295,7 @@ export class TreeVisitor extends ParseTreeWalker {
documentation.push('```python\n' + assignmentDoc.join('\n') + '\n```');
}

// node.typeAnnotationComment
this.document.symbols.push(
new scip.SymbolInformation({
symbol: this.getScipSymbol(dec.node).value,
documentation,
})
);
this.emitSymbolInformationOnce(dec.node, this.getScipSymbol(dec.node), documentation);
}
}
}
Expand Down Expand Up @@ -1161,15 +1139,15 @@ export class TreeVisitor extends ParseTreeWalker {
}
}

return Symbols.makeTerm(this.getScipSymbol(enclosingSuite || parent), (node as NameNode).value);
// An annotation describes the name; it does not introduce another scope.
const scope =
enclosingSuite || (parent.nodeType === ParseNodeType.TypeAnnotation ? parent.parent! : parent);
return Symbols.makeTerm(this.getScipSymbol(scope), (node as NameNode).value);
}
case ParseNodeType.TypeAnnotation: {
switch (node.valueExpression.nodeType) {
case ParseNodeType.Name: {
return Symbols.makeTerm(
this.getScipSymbol(ParseTreeUtils.getEnclosingSuite(node) || node.parent!),
node.valueExpression.value
);
return this.getScipSymbol(node.valueExpression);
}
default: {
softAssert(false, 'Unhandled type annotation');
Expand Down
98 changes: 98 additions & 0 deletions packages/pyright-scip/test/test-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,103 @@ version = "16.05"`,
}
}

function testAnnotatedVariables(): void {
// macOS temporary directories can be symlinked; match the analyzer's resolved file paths.
const projectRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'scip-annotations-')));
const source = `initialized: int = 1
uninitialized: int
print(initialized, uninitialized)
class Base:
field: int = 2
pending: int
def method(self):
method_local: int = 3
return method_local
class Child(Base):
pass
def function():
function_local: int = 4
return function_local
obj = Child()
obj.method()
module_assigned = 5
module_assigned: int
def assigned_first():
local_assigned = 6
local_assigned: int
return local_assigned
class AssignedFirst:
class_assigned = 7
class_assigned: int
`;
try {
fs.writeFileSync(path.join(projectRoot, 'main.py'), source);
indexAction({
projectName: 'annotations',
projectVersion: '1.0',
cwd: projectRoot,
output: 'index.scip',
environment: path.resolve('snapshots/testEnv.json'),
infer: { projectVersionFromCommit: false },
quiet: true,
dev: false,
showProgressRateLimit: undefined,
});
const index = scip.Index.deserializeBinary(fs.readFileSync(path.join(projectRoot, 'index.scip')));
const document = index.documents.find((doc) => doc.relative_path === 'main.py')!;
for (const [line, expected] of [
[0, 'main/initialized.'],
[1, 'main/uninitialized.'],
[4, 'main/Base#field.'],
[5, 'main/Base#pending.'],
[7, 'local '],
[12, 'local '],
[16, 'main/module_assigned.'],
[19, 'local '],
[23, 'main/AssignedFirst#class_assigned.'],
] as const) {
const definition = document.occurrences.find(
(occurrence) =>
occurrence.range[0] === line &&
occurrence.range.length === 3 &&
occurrence.range[2] > occurrence.range[1] &&
(occurrence.symbol_roles & scip.SymbolRole.Definition) !== 0
)!;
assert.ok(definition, `Missing definition on line ${line}`);
assert.ok(
expected === 'local ' ? definition.symbol.startsWith(expected) : definition.symbol.endsWith(expected),
definition.symbol
);
const information = document.symbols.filter((symbol) => symbol.symbol === definition.symbol);
assert.strictEqual(information.length, 1, `Expected one information record for ${definition.symbol}`);
assert.ok(information[0].documentation.length > 0);
if ([0, 1, 7, 12].includes(line)) {
assert.ok(
document.occurrences.some(
(occurrence) =>
occurrence.symbol === definition.symbol &&
occurrence.symbol_roles === scip.SymbolRole.ReadAccess
)
);
}
}
assert.ok(
document.occurrences.some(
(occurrence) => occurrence.range[0] === 15 && occurrence.symbol.endsWith('main/Base#method().')
)
);
assert.ok(!document.occurrences.some((occurrence) => occurrence.symbol.endsWith('main/Child#method().')));
for (const symbol of document.symbols.filter((symbol) => symbol.symbol.startsWith('local '))) {
assert.ok(
document.occurrences.some((occurrence) => occurrence.symbol === symbol.symbol),
`Orphan local information: ${symbol.symbol}`
);
}
} finally {
fs.rmSync(projectRoot, { recursive: true, force: true });
}
}

function testImportRoles(): void {
// macOS temporary directories can be symlinked; match the analyzer's resolved file paths.
const projectRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'scip-import-roles-')));
Expand Down Expand Up @@ -542,6 +639,7 @@ function testMissingProjectVersion(): void {

function unitTests(): void {
testPyprojectParsing();
testAnnotatedVariables();
testImportRoles();
testExternalDocumentation();
testPyprojectLineEndings();
Expand Down
Loading