pyrefly icon indicating copy to clipboard operation
pyrefly copied to clipboard

Include class name in `pyrefly report`

Open MarcoGorelli opened this issue 3 months ago • 2 comments

I'm trying to use pyrefly report to check pandas type completeness, and have noticed that the class name isn't included

Example:

# foo/__init__.py
def foo(a) -> None:
    return None

class Bar:
    def foo(self, a: int) -> None:
        return None

Then, running pyrefly report foo/:

{
  "/home/marcogorelli/tmp-repo/foo/__init__.py": {
    "line_count": 8,
    "functions": [
      {
        "name": "foo",
        "return_annotation": "None",
        "parameters": [
          {
            "name": "a",
            "annotation": null,
            "location": {
              "start": {
                "line": 1,
                "column": 9
              },
              "end": {
                "line": 1,
                "column": 10
              }
            }
          }
        ],
        "location": {
          "start": {
            "line": 1,
            "column": 1
          },
          "end": {
            "line": 2,
            "column": 16
          }
        }
      },
      {
        "name": "foo",
        "return_annotation": "None",
        "parameters": [
          {
            "name": "self",
            "annotation": null,
            "location": {
              "start": {
                "line": 5,
                "column": 13
              },
              "end": {
                "line": 5,
                "column": 17
              }
            }
          },
          {
            "name": "a",
            "annotation": "int",
            "location": {
              "start": {
                "line": 5,
                "column": 19
              },
              "end": {
                "line": 5,
                "column": 25
              }
            }
          }
        ],
        "location": {
          "start": {
            "line": 5,
            "column": 5
          },
          "end": {
            "line": 6,
            "column": 20
          }
        }
      }
    ],
    "suppressions": []
  }
}

Running pyright --verifytypes foo --outputjson:

{
    "version": "1.1.394",
    "time": "1764413054007",
    "generalDiagnostics": [],
    "summary": {
        "filesAnalyzed": 1,
        "errorCount": 0,
        "warningCount": 0,
        "informationCount": 0,
        "timeInSec": 0.82
    },
    "typeCompleteness": {
        "packageName": "foo",
        "packageRootDirectory": "/home/marcogorelli/tmp-repo/foo",
        "moduleName": "foo",
        "moduleRootDirectory": "/home/marcogorelli/tmp-repo/foo",
        "ignoreUnknownTypesFromImports": false,
        "pyTypedPath": "/home/marcogorelli/tmp-repo/foo/py.typed",
        "exportedSymbolCounts": {
            "withKnownType": 2,
            "withAmbiguousType": 0,
            "withUnknownType": 1
        },
        "otherSymbolCounts": {
            "withKnownType": 0,
            "withAmbiguousType": 0,
            "withUnknownType": 0
        },
        "missingFunctionDocStringCount": 2,
        "missingClassDocStringCount": 1,
        "missingDefaultParamCount": 0,
        "completenessScore": 0.6666666666666666,
        "modules": [
            {
                "name": "foo"
            }
        ],
        "symbols": [
            {
                "category": "function",
                "name": "foo.foo",
                "referenceCount": 1,
                "isExported": true,
                "isTypeKnown": false,
                "isTypeAmbiguous": false,
                "diagnostics": [
                    {
                        "file": "/home/marcogorelli/tmp-repo/foo/__init__.py",
                        "severity": "warning",
                        "message": "No docstring found for function \"foo.foo\"",
                        "range": {
                            "start": {
                                "line": 0,
                                "character": 4
                            },
                            "end": {
                                "line": 0,
                                "character": 7
                            }
                        }
                    },
                    {
                        "file": "/home/marcogorelli/tmp-repo/foo/__init__.py",
                        "severity": "error",
                        "message": "Type annotation for parameter \"a\" is missing",
                        "range": {
                            "start": {
                                "line": 0,
                                "character": 4
                            },
                            "end": {
                                "line": 0,
                                "character": 7
                            }
                        }
                    }
                ]
            },
            {
                "category": "class",
                "name": "foo.Bar",
                "referenceCount": 1,
                "isExported": true,
                "isTypeKnown": true,
                "isTypeAmbiguous": false,
                "diagnostics": [
                    {
                        "file": "",
                        "severity": "warning",
                        "message": "No docstring found for class \"foo.Bar\""
                    }
                ]
            },
            {
                "category": "method",
                "name": "foo.Bar.foo",
                "referenceCount": 1,
                "isExported": true,
                "isTypeKnown": true,
                "isTypeAmbiguous": false,
                "diagnostics": [
                    {
                        "file": "/home/marcogorelli/tmp-repo/foo/__init__.py",
                        "severity": "warning",
                        "message": "No docstring found for function \"foo.Bar.foo\"",
                        "range": {
                            "start": {
                                "line": 4,
                                "character": 8
                            },
                            "end": {
                                "line": 4,
                                "character": 11
                            }
                        }
                    }
                ]
            }
        ]
    }
}

Note how the pyrefly report gives me two functions with "name": "foo", whereas pyright gives me foo.foo and foo.Bar.foo, making it much easier to distinguish them

In order to make it feasible to part the output and do completeness analysis, I'd like to request that pyrefly also includes the full names of functions

MarcoGorelli avatar Nov 29 '25 10:11 MarcoGorelli

@yangdanny97 Are you sure that fix is correct? I think just using the bindings name will never "be enough" and we likely want to actuallt resolve the function.

class A:
    def method_on_a(self) -> str:

        class B:
            def method_on_b(self) -> str:
                return "Method on B"
        
        return B().method_on_b()

On main gives:

"line_count": 9,
    "functions": [
      {
        "name": "__unknown__.A.method_on_a",
        "return_annotation": "str",
        "parameters": [
          {
            "name": "self",
            "annotation": null,
            "location": {
              "start": {
                "line": 2,
                "column": 21
              },
              "end": {
                "line": 2,
                "column": 25
              }
            }
          }
        ],
        "location": {
          "start": {
            "line": 2,
            "column": 5
          },
          "end": {
            "line": 8,
            "column": 33
          }
        }
      },
      {
        "name": "__unknown__.B.method_on_b",
        "return_annotation": "str",
        "parameters": [
          {
            "name": "self",
            "annotation": null,
            "location": {
              "start": {
                "line": 5,
                "column": 29
              },
              "end": {
                "line": 5,
                "column": 33
              }
            }
          }
        ],
        "location": {
          "start": {
            "line": 5,
            "column": 13
          },
          "end": {
            "line": 6,
            "column": 37
          }
        }
      }
    ],
    "suppressions": []
  }

I don't think the __unknown__ is what is expected here.

DanielNoord avatar Dec 01 '25 21:12 DanielNoord

For some reason it thinks your module name is __unknown__, that's weird. I'll look into it some more tomorrow.

By "actuallt resolve the function" do you mean encoding method_on_b as A.method_on_a.B.method_on_b?

There is no guaranteed way to uniquely identify a class or method using a qualified path, since you can have function or method definitions anywhere

For example, this would still have duplicates, unless you somehow also encode the conditional into the path:

if something:
    def foo(x: int): pass
else:
    def foo(x: int): pass

So we can't rely on the paths being unique even if we do what you say, and any ambiguities will have to be resolved by position.

I think including all enclosing classes/methods in the qualified path is a reasonable choice, but I don't necessarily think it's an improvement over what was merged today unless we're working in codebases that have many nested classes with the same name.

yangdanny97 avatar Dec 02 '25 01:12 yangdanny97

I've removed the module name prefix, so now it should only show the immediately enclosing class + the function's name.

yangdanny97 avatar Dec 05 '25 14:12 yangdanny97

I guess that works. There will be some edge cases that are still not covered, but I'd tackle those in separate issues.

DanielNoord avatar Dec 06 '25 09:12 DanielNoord

So to summarize (whenever I have some time to get back to this):

  • show nested classes/methods
  • don't show __unknown__ for unknown module paths

yangdanny97 avatar Dec 11 '25 15:12 yangdanny97