Updated package type verifier to differentiate between "unknown" and "ambiguous" types.

This commit is contained in:
Eric Traut 2022-02-24 10:30:25 -07:00
parent 99fda8c173
commit c9b9676e21
4 changed files with 345 additions and 172 deletions

View File

@ -13,13 +13,11 @@ These recommendations are intended to provide the following benefits:
## Inlined Type Annotations and Type Stubs
[PEP 561](https://www.python.org/dev/peps/pep-0561/) documents several ways type information can be delivered for a library: inlined type annotations, type stub files included in the package, a separate companion type stub package, and type stubs in the typeshed repository. Some of these options fall short on delivering the benefits above. We therefore provide the following more specific guidance to library authors.
*All libraries should include inlined type annotations for the functions, classes, methods, and constants that comprise the public interface for the library.*
All libraries should include inlined type annotations for the functions, classes, methods, and constants that comprise the public interface for the library.
Inlined type annotations should be included directly within the source code that ships with the package. Of the options listed in PEP 561, inlined type annotations offer the most benefits. They typically require the least effort to add and maintain, they are always consistent with the implementation, and docstrings and default parameter values are readily available, allowing language servers to enhance the development experience.
There are cases where inlined type annotations are not possible — most notably when a librarys exposed functionality is implemented in a language other than Python.
*Libraries that expose symbols implemented in languages other than Python should include stub (“.pyi”) files that describe the types for those symbols. These stubs should also contain docstrings and default parameter values.*
There are cases where inlined type annotations are not possible — most notably when a librarys exposed functionality is implemented in a language other than Python. Libraries that expose symbols implemented in languages other than Python should include stub (“.pyi”) files that describe the types for those symbols. These stubs should also contain docstrings and default parameter values.
In many existing type stubs (such as those found in typeshed), default parameter values are replaced with with “...” and all docstrings are removed. We recommend that default values and docstrings remain within the type stub file so language servers can display this information to developers.
@ -74,7 +72,6 @@ Variables:
Type annotations can be omitted in a few specific cases where the type is obvious from the context:
* A class or instance variable does not require an annotation if the class inherits from another class that has provided an annotation for the variable of the same name. The type is inherited from the parent class in this case.
* Constants that are assigned simple literal values (e.g. `RED = '#F00'` or `MAX_TIMEOUT = 50` or `room_temperature: Final = 20`). A constant is a symbol that is assigned only once and is either annotated with `Final` or is named in all-caps. A constant that is not assigned a simple literal value requires explicit annotations, preferably with a `Final` annotation (e.g. `WOODWINDS: Final[List[str]] = ['Oboe', 'Bassoon']`).
* Enum values within an Enum class do not require annotations because they take on the type of the Enum class.
* Type aliases do not require annotations. A type alias is a symbol that is defined at a module level with a single assignment where the assigned value is an instantiable type, as opposed to a class instance (e.g. `Foo = Callable[[Literal["a", "b"]], Union[int, str]]` or `Bar = Optional[MyGenericClass[int]]`).
@ -82,14 +79,26 @@ Type annotations can be omitted in a few specific cases where the type is obviou
* The return type for an `__init__` method does not need to be specified, since it is always `None`.
* The following module-level symbols do not require type annotations: `__all__`,`__author__`, `__copyright__`, `__email__`, `__license__`, `__title__`, `__uri__`, `__version__`.
* The following class-level symbols do not require type annotations: `__class__`, `__dict__`, `__doc__`, `__module__`, `__slots__`.
* A variable is assigned in only one location using a simple assignment expression and the right-hand side of the assignment is a literal value (e.g. `1`, `3.14`, `"hi"`, or `MyEnum.Value`) or an identifier that has a known type that doesn't depend on type narrowing logic.
### Examples of known and unknown types
### Ambiguous Types
When a symbol is missing a type annotation, a type checker may be able to infer its type based on contextual information. However, type inference rules are not standardized and differ between type checkers. A symbol is said to have an “ambiguous type” if its type may be inferred differently between different Python type checkers. This can lead to a bad experience for consumers of the library.
Ambiguous types can be avoided by providing explicit type annotations.
### Examples of known, ambiguous and unknown types
```python
# Variable with unknown type
# Variable with known type (unambiguous because it uses a literal assignment)
a = 3
# Variable with ambiguous type
a = [3, 4, 5]
# Variable with known type
# Variable with known (declared) type
a: List[int] = [3, 4, 5]
# Type alias with partially unknown type (because type
@ -148,7 +157,7 @@ class MyClass:
# Class with partially unknown type
class MyClass:
# Missing type annotation for class variable
height = 2.0
height = None
# Missing input parameter annotations
def __init__(self, name, age):
@ -161,9 +170,9 @@ class MyClass:
...
# Class with partially unknown type
class BaseClass:
class BaseClass1:
# Missing type annotation
height = 2.0
height: = 2.0
# Missing type annotation
def get_stuff(self):
@ -171,12 +180,21 @@ class BaseClass:
# Class with known type (because it overrides all symbols
# exposed by BaseClass that have incomplete types)
class DerivedClass(BaseClass):
class DerivedClass1(BaseClass1):
height: float
def get_stuff(self) -> str:
...
# Class with known type
class BaseClass2:
height: float = 2.0
# Class with ambiguous type
class DerivedClass2(BaseClass2):
# Missing type annotation, could be inferred as float or int
height = 1
# Class with partially unknown type because base class
# (dict) is generic, and type arguments are not specified.
class DictSubclass(dict):
@ -189,7 +207,7 @@ Pyright provides a feature that allows library authors to verify type completene
`pyright --verifytypes <lib>`
Pyright will analyze the library, identify all symbols that comprise the interface to the library and emit errors for any symbols whose types are unknown. It also produces a “type completeness score” which is the percentage of symbols with known types.
Pyright will analyze the library, identify all symbols that comprise the interface to the library and emit errors for any symbols whose types are ambiguous or unknown. It also produces a “type completeness score” which is the percentage of symbols with known types.
To see additional details (including a full list of symbols in the library), append the `--verbose` option.

View File

@ -23,10 +23,13 @@ export enum SymbolCategory {
TypeAlias,
}
// The order of these is important. Status values with higher numbers are
// considered "worse" than status values with lower numbers.
export const enum TypeKnownStatus {
Known,
PartiallyUnknown,
Unknown,
Known = 0, // Type is fully known (declared)
Ambiguous = 1, // Type is inferred and potentially ambiguous (may differ by type checker)
PartiallyUnknown = 2, // Part of the type is unknown
Unknown = 3, // The type is completely unknown
}
export interface SymbolInfo {

View File

@ -38,6 +38,7 @@ import {
isClass,
isInstantiableClass,
isModule,
isTypeSame,
isUnknown,
ModuleType,
Type,
@ -342,7 +343,7 @@ export class PackageTypeVerifier {
const parseTree = sourceFile.getParseResults()!.parseTree;
const moduleScope = getScopeForNode(parseTree)!;
this._verifySymbolsInSymbolTable(
this._getTypeKnownStatusForSymbolTable(
report,
module.name,
moduleScope.symbolTable,
@ -436,19 +437,19 @@ export class PackageTypeVerifier {
return report.ignoreExternal && !fullTypeName.startsWith(report.packageName);
}
private _verifySymbolsInSymbolTable(
private _getTypeKnownStatusForSymbolTable(
report: PackageTypeReport,
scopeName: string,
symbolTable: SymbolTable,
scopeType: ScopeType,
publicSymbolMap: PublicSymbolMap,
overrideSymbolCallback?: (name: string, symbol: Symbol) => Symbol
): boolean {
): TypeKnownStatus {
if (this._shouldIgnoreType(report, scopeName)) {
return true;
return TypeKnownStatus.Known;
}
let isKnown = true;
let knownStatus = TypeKnownStatus.Known;
symbolTable.forEach((symbol, name) => {
if (
@ -469,10 +470,29 @@ export class PackageTypeVerifier {
return;
}
let symbolType = this._program.getTypeForSymbol(symbol);
let usesAmbiguousOverride = false;
let baseSymbolType: Type | undefined;
let childSymbolType: Type | undefined;
if (overrideSymbolCallback) {
symbol = overrideSymbolCallback(name, symbol);
const baseTypeSymbol = overrideSymbolCallback(name, symbol);
if (baseTypeSymbol !== symbol) {
childSymbolType = symbolType;
baseSymbolType = this._program.getTypeForSymbol(baseTypeSymbol);
// If the inferred type is ambiguous or the declared base class type is
// not the same type as the inferred type, mark it as ambiguous because
// different type checkers will get different results.
if (TypeBase.isAmbiguous(childSymbolType) || !isTypeSame(baseSymbolType, childSymbolType)) {
usesAmbiguousOverride = true;
}
symbolType = baseSymbolType;
}
}
const symbolType = this._program.getTypeForSymbol(symbol);
const typedDecls = symbol.getTypedDeclarations();
const primaryDecl = typedDecls.length > 0 ? typedDecls[typedDecls.length - 1] : undefined;
@ -514,31 +534,79 @@ export class PackageTypeVerifier {
this._addSymbol(report, symbolInfo);
if (!this._isSymbolTypeImplied(scopeType, name)) {
this._validateSymbolType(report, symbolInfo, symbolType, declRange, declPath, publicSymbolMap);
this._getSymbolTypeKnownStatus(
report,
symbolInfo,
symbolType,
declRange,
declPath,
publicSymbolMap
);
}
}
if (symbolInfo.typeKnownStatus !== TypeKnownStatus.Known) {
isKnown = false;
if (usesAmbiguousOverride) {
const decls = symbol.getDeclarations();
const primaryDecl = decls.length > 0 ? decls[decls.length - 1] : undefined;
const declRange = primaryDecl?.range || getEmptyRange();
const declPath = primaryDecl?.path || '';
const extraInfo = new DiagnosticAddendum();
if (baseSymbolType) {
extraInfo.addMessage(
`Type declared in base class is "${this._program.printType(
baseSymbolType,
/* expandTypeAlias */ false
)}"`
);
}
if (childSymbolType) {
extraInfo.addMessage(
`Type inferred in child class is "${this._program.printType(
childSymbolType,
/* expandTypeAlias */ false
)}"`
);
if (TypeBase.isAmbiguous(childSymbolType)) {
extraInfo.addMessage(
'Inferred child class type is missing type annotation and could be inferred differently by type checkers'
);
}
}
this._addSymbolError(
symbolInfo,
`Ambiguous base class override` + extraInfo.getString(),
declRange,
declPath
);
symbolInfo.typeKnownStatus = this._updateKnownStatusIfWorse(
symbolInfo.typeKnownStatus,
TypeKnownStatus.Ambiguous
);
}
knownStatus = this._updateKnownStatusIfWorse(knownStatus, symbolInfo.typeKnownStatus);
}
});
return isKnown;
return knownStatus;
}
// Determines whether the type for the symbol in question is fully known.
// If not, it adds diagnostics to the symbol information and updates the
// typeKnownStatus field.
private _validateSymbolType(
private _getSymbolTypeKnownStatus(
report: PackageTypeReport,
symbolInfo: SymbolInfo,
type: Type,
declRange: Range,
declFilePath: string,
publicSymbolMap: PublicSymbolMap
): boolean {
let isKnown = true;
): TypeKnownStatus {
let knownStatus = TypeKnownStatus.Known;
if (type.typeAliasInfo && type.typeAliasInfo.typeArguments) {
type.typeAliasInfo.typeArguments.forEach((typeArg, index) => {
@ -549,7 +617,7 @@ export class PackageTypeVerifier {
declRange,
declFilePath
);
isKnown = false;
knownStatus = TypeKnownStatus.Unknown;
} else if (isPartlyUnknown(typeArg)) {
this._addSymbolError(
symbolInfo,
@ -559,18 +627,33 @@ export class PackageTypeVerifier {
declRange,
declFilePath
);
isKnown = false;
knownStatus = TypeKnownStatus.PartiallyUnknown;
}
});
}
if (TypeBase.isAmbiguous(type) && !isUnknown(type)) {
const ambiguousDiag = new DiagnosticAddendum();
ambiguousDiag.addMessage(
`Inferred type is "${this._program.printType(type, /* expandTypeAlias */ false)}"`
);
this._addSymbolError(
symbolInfo,
'Type is missing type annotation and could be inferred differently by type checkers' +
ambiguousDiag.getString(),
declRange,
declFilePath
);
knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.Ambiguous);
}
switch (type.category) {
case TypeCategory.Unbound:
case TypeCategory.Any:
case TypeCategory.None:
case TypeCategory.Never:
case TypeCategory.TypeVar:
return isKnown;
break;
case TypeCategory.Unknown: {
this._addSymbolError(
@ -581,30 +664,32 @@ export class PackageTypeVerifier {
declRange,
declFilePath
);
symbolInfo.typeKnownStatus = TypeKnownStatus.Unknown;
return false;
knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.Unknown);
break;
}
case TypeCategory.Union: {
doForEachSubtype(type, (subtype) => {
if (
!this._validateSymbolType(report, symbolInfo, subtype, declRange, declFilePath, publicSymbolMap)
) {
isKnown = false;
}
knownStatus = this._updateKnownStatusIfWorse(
knownStatus,
this._getSymbolTypeKnownStatus(
report,
symbolInfo,
subtype,
declRange,
declFilePath,
publicSymbolMap
)
);
});
if (!isKnown) {
symbolInfo.typeKnownStatus = TypeKnownStatus.PartiallyUnknown;
}
return isKnown;
break;
}
case TypeCategory.OverloadedFunction: {
for (const overload of type.overloads) {
if (
!this._validateSymbolType(
knownStatus = this._updateKnownStatusIfWorse(
knownStatus,
this._getSymbolTypeKnownStatus(
report,
symbolInfo,
overload,
@ -612,28 +697,27 @@ export class PackageTypeVerifier {
declFilePath,
publicSymbolMap
)
) {
isKnown = false;
}
);
}
if (!isKnown) {
symbolInfo.typeKnownStatus = TypeKnownStatus.PartiallyUnknown;
}
return isKnown;
break;
}
case TypeCategory.Function: {
if (!this._shouldIgnoreType(report, type.details.fullName)) {
if (
!this._validateFunctionType(report, type, publicSymbolMap, symbolInfo, declRange, declFilePath)
) {
symbolInfo.typeKnownStatus = TypeKnownStatus.PartiallyUnknown;
isKnown = false;
}
knownStatus = this._updateKnownStatusIfWorse(
knownStatus,
this._getFunctionTypeKnownStatus(
report,
type,
publicSymbolMap,
symbolInfo,
declRange,
declFilePath
)
);
}
return isKnown;
break;
}
case TypeCategory.Class: {
@ -650,8 +734,9 @@ export class PackageTypeVerifier {
return;
}
if (
!this._validateSymbolType(
knownStatus = this._updateKnownStatusIfWorse(
knownStatus,
this._getSymbolTypeKnownStatus(
report,
symbolInfo,
accessType,
@ -659,12 +744,10 @@ export class PackageTypeVerifier {
'',
publicSymbolMap
)
) {
isKnown = false;
}
);
});
return isKnown;
break;
}
if (!this._shouldIgnoreType(report, type.details.fullName)) {
@ -685,7 +768,7 @@ export class PackageTypeVerifier {
declRange,
declFilePath
);
isKnown = false;
knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.Unknown);
} else if (isPartlyUnknown(typeArg)) {
const diag = new DiagnosticAddendum();
diag.addMessage(`Type is ${this._program.printType(typeArg, /* expandTypeAlias */ false)}`);
@ -697,16 +780,12 @@ export class PackageTypeVerifier {
declRange,
declFilePath
);
isKnown = false;
knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.PartiallyUnknown);
}
});
}
if (!isKnown) {
symbolInfo.typeKnownStatus = TypeKnownStatus.PartiallyUnknown;
}
return isKnown;
break;
}
case TypeCategory.Module: {
@ -719,20 +798,21 @@ export class PackageTypeVerifier {
declRange,
declFilePath
);
isKnown = false;
knownStatus = this._updateKnownStatusIfWorse(knownStatus, moduleSymbol.typeKnownStatus);
}
}
if (!isKnown) {
symbolInfo.typeKnownStatus = TypeKnownStatus.PartiallyUnknown;
}
return isKnown;
break;
}
}
// Downgrade the symbol's type known status info.
symbolInfo.typeKnownStatus = this._updateKnownStatusIfWorse(symbolInfo.typeKnownStatus, knownStatus);
return knownStatus;
}
private _validateFunctionType(
private _getFunctionTypeKnownStatus(
report: PackageTypeReport,
type: FunctionType,
publicSymbolMap: PublicSymbolMap,
@ -740,8 +820,8 @@ export class PackageTypeVerifier {
declRange?: Range,
declFilePath?: string,
diag?: DiagnosticAddendum
): boolean {
let isKnown = true;
): TypeKnownStatus {
let knownStatus = TypeKnownStatus.Known;
// If the file path wasn't provided, try to get it from the type.
if (type.details.declaration && !declFilePath) {
@ -774,7 +854,7 @@ export class PackageTypeVerifier {
`Type annotation for parameter "${param.name}" is missing`
);
}
isKnown = false;
knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.Unknown);
}
} else if (isUnknown(param.type)) {
if (symbolInfo) {
@ -788,13 +868,21 @@ export class PackageTypeVerifier {
diag.createAddendum().addMessage(`Type of parameter "${param.name}" is unknown`);
}
}
isKnown = false;
knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.Unknown);
} else {
const extraInfo = new DiagnosticAddendum();
if (!this._isTypeKnown(report, param.type, publicSymbolMap, extraInfo.createAddendum())) {
const paramKnownStatus = this._getTypeKnownStatus(
report,
param.type,
publicSymbolMap,
extraInfo.createAddendum()
);
if (paramKnownStatus !== TypeKnownStatus.Known) {
extraInfo.addMessage(
`Parameter type is "${this._program.printType(param.type, /* expandTypeAlias */ false)}"`
);
if (symbolInfo) {
this._addSymbolError(
symbolInfo,
@ -803,12 +891,14 @@ export class PackageTypeVerifier {
declFilePath || ''
);
}
if (diag) {
const subDiag = diag.createAddendum();
subDiag.addMessage(`Type of parameter "${param.name}" is partially unknown`);
subDiag.addAddendum(extraInfo);
}
isKnown = false;
knownStatus = this._updateKnownStatusIfWorse(knownStatus, paramKnownStatus);
}
}
}
@ -824,23 +914,24 @@ export class PackageTypeVerifier {
declFilePath || ''
);
}
isKnown = false;
knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.Unknown);
} else {
const extraInfo = new DiagnosticAddendum();
if (
!this._isTypeKnown(
report,
type.details.declaredReturnType,
publicSymbolMap,
extraInfo.createAddendum()
)
) {
const returnTypeKnownStatus = this._getTypeKnownStatus(
report,
type.details.declaredReturnType,
publicSymbolMap,
extraInfo.createAddendum()
);
if (returnTypeKnownStatus !== TypeKnownStatus.Known) {
extraInfo.addMessage(
`Return type is "${this._program.printType(
type.details.declaredReturnType,
/* expandTypeAlias */ false
)}"`
);
if (symbolInfo) {
this._addSymbolError(
symbolInfo,
@ -849,12 +940,14 @@ export class PackageTypeVerifier {
declFilePath || ''
);
}
if (diag) {
const subDiag = diag.createAddendum();
subDiag.addMessage(`Return type is partially unknown`);
subDiag.addAddendum(extraInfo);
}
isKnown = false;
knownStatus = this._updateKnownStatusIfWorse(knownStatus, returnTypeKnownStatus);
}
}
} else {
@ -871,7 +964,7 @@ export class PackageTypeVerifier {
if (diag) {
diag.createAddendum().addMessage(`Return type annotation is missing`);
}
isKnown = false;
knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.Unknown);
}
}
@ -904,11 +997,11 @@ export class PackageTypeVerifier {
report.missingDefaultParamCount++;
}
if (!isKnown && symbolInfo) {
symbolInfo.typeKnownStatus = TypeKnownStatus.PartiallyUnknown;
if (symbolInfo) {
symbolInfo.typeKnownStatus = this._updateKnownStatusIfWorse(symbolInfo.typeKnownStatus, knownStatus);
}
return isKnown;
return knownStatus;
}
private _getSymbolForClass(
@ -948,43 +1041,54 @@ export class PackageTypeVerifier {
report.missingClassDocStringCount++;
}
if (
!this._verifySymbolsInSymbolTable(
report,
type.details.fullName,
type.details.fields,
ScopeType.Class,
publicSymbolMap,
(name: string, symbol: Symbol) => {
// If the symbol within this class is lacking a type declaration,
// see if we can find a same-named symbol in a parent class with
// a type declaration.
if (!symbol.hasTypedDeclarations()) {
for (const mroClass of type.details.mro.slice(1)) {
if (isClass(mroClass)) {
const overrideSymbol = mroClass.details.fields.get(name);
if (overrideSymbol && overrideSymbol.hasTypedDeclarations()) {
return overrideSymbol;
}
const symbolTableTypeKnownStatus = this._getTypeKnownStatusForSymbolTable(
report,
type.details.fullName,
type.details.fields,
ScopeType.Class,
publicSymbolMap,
(name: string, symbol: Symbol) => {
// If the symbol within this class is lacking a type declaration,
// see if we can find a same-named symbol in a parent class with
// a type declaration.
if (!symbol.hasTypedDeclarations()) {
for (const mroClass of type.details.mro.slice(1)) {
if (isClass(mroClass)) {
const overrideSymbol = mroClass.details.fields.get(name);
if (overrideSymbol && overrideSymbol.hasTypedDeclarations()) {
return overrideSymbol;
}
}
}
return symbol;
}
)
) {
symbolInfo.typeKnownStatus = TypeKnownStatus.PartiallyUnknown;
}
return symbol;
}
);
symbolInfo.typeKnownStatus = this._updateKnownStatusIfWorse(
symbolInfo.typeKnownStatus,
symbolTableTypeKnownStatus
);
// Add information for the metaclass.
if (type.details.effectiveMetaclass) {
if (!isInstantiableClass(type.details.effectiveMetaclass)) {
this._addSymbolError(symbolInfo, `Type of metaclass unknown`, getEmptyRange(), '');
symbolInfo.typeKnownStatus = TypeKnownStatus.PartiallyUnknown;
symbolInfo.typeKnownStatus = this._updateKnownStatusIfWorse(
symbolInfo.typeKnownStatus,
TypeKnownStatus.PartiallyUnknown
);
} else {
const diag = new DiagnosticAddendum();
if (!this._isTypeKnown(report, type.details.effectiveMetaclass, publicSymbolMap, diag)) {
const metaclassKnownStatus = this._getTypeKnownStatus(
report,
type.details.effectiveMetaclass,
publicSymbolMap,
diag
);
if (metaclassKnownStatus !== TypeKnownStatus.Known) {
this._addSymbolError(
symbolInfo,
`Type of metaclass "${type.details.effectiveMetaclass}" is partially unknown` +
@ -992,7 +1096,10 @@ export class PackageTypeVerifier {
getEmptyRange(),
''
);
symbolInfo.typeKnownStatus = TypeKnownStatus.PartiallyUnknown;
symbolInfo.typeKnownStatus = this._updateKnownStatusIfWorse(
symbolInfo.typeKnownStatus,
metaclassKnownStatus
);
}
}
}
@ -1001,7 +1108,10 @@ export class PackageTypeVerifier {
type.details.baseClasses.forEach((baseClass) => {
if (!isInstantiableClass(baseClass)) {
this._addSymbolError(symbolInfo, `Type of base class unknown`, getEmptyRange(), '');
symbolInfo.typeKnownStatus = TypeKnownStatus.PartiallyUnknown;
symbolInfo.typeKnownStatus = this._updateKnownStatusIfWorse(
symbolInfo.typeKnownStatus,
TypeKnownStatus.PartiallyUnknown
);
} else {
// Handle "tuple" specially. Even though it's a generic class, it
// doesn't require a type argument.
@ -1010,14 +1120,20 @@ export class PackageTypeVerifier {
}
const diag = new DiagnosticAddendum();
if (!this._isTypeKnown(report, baseClass, publicSymbolMap, diag)) {
const baseClassTypeStatus = this._getTypeKnownStatus(report, baseClass, publicSymbolMap, diag);
if (baseClassTypeStatus !== TypeKnownStatus.Known) {
this._addSymbolError(
symbolInfo,
`Type of base class "${baseClass.details.fullName}" is partially unknown` + diag.getString(),
getEmptyRange(),
''
);
symbolInfo.typeKnownStatus = TypeKnownStatus.PartiallyUnknown;
symbolInfo.typeKnownStatus = this._updateKnownStatusIfWorse(
symbolInfo.typeKnownStatus,
baseClassTypeStatus
);
}
}
});
@ -1050,22 +1166,29 @@ export class PackageTypeVerifier {
this._addSymbol(report, symbolInfo);
if (
!this._verifySymbolsInSymbolTable(report, type.moduleName, type.fields, ScopeType.Module, publicSymbolMap)
) {
symbolInfo.typeKnownStatus = TypeKnownStatus.PartiallyUnknown;
}
const symbolTableTypeKnownStatus = this._getTypeKnownStatusForSymbolTable(
report,
type.moduleName,
type.fields,
ScopeType.Module,
publicSymbolMap
);
symbolInfo.typeKnownStatus = this._updateKnownStatusIfWorse(
symbolInfo.typeKnownStatus,
symbolTableTypeKnownStatus
);
return symbolInfo;
}
private _isTypeKnown(
private _getTypeKnownStatus(
report: PackageTypeReport,
type: Type,
publicSymbolMap: PublicSymbolMap,
diag: DiagnosticAddendum
): boolean {
let isKnown = true;
): TypeKnownStatus {
let knownStatus = TypeKnownStatus.Known;
if (type.typeAliasInfo && type.typeAliasInfo.typeArguments) {
type.typeAliasInfo.typeArguments.forEach((typeArg, index) => {
@ -1073,54 +1196,62 @@ export class PackageTypeVerifier {
diag.addMessage(
`Type argument ${index + 1} for type alias "${type.typeAliasInfo!.name}" has unknown type`
);
isKnown = false;
knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.Unknown);
} else if (isPartlyUnknown(typeArg)) {
diag.addMessage(
`Type argument ${index + 1} for type alias "${
type.typeAliasInfo!.name
}" has partially unknown type`
);
isKnown = false;
knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.PartiallyUnknown);
}
});
}
if (TypeBase.isAmbiguous(type)) {
knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.Ambiguous);
}
switch (type.category) {
case TypeCategory.Unbound:
case TypeCategory.Any:
case TypeCategory.None:
case TypeCategory.Never:
case TypeCategory.TypeVar:
return isKnown;
break;
case TypeCategory.Unknown: {
return false;
knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.Unknown);
break;
}
case TypeCategory.Union: {
doForEachSubtype(type, (subtype) => {
if (!this._isTypeKnown(report, subtype, publicSymbolMap, diag.createAddendum())) {
isKnown = false;
}
knownStatus = this._updateKnownStatusIfWorse(
knownStatus,
this._getTypeKnownStatus(report, subtype, publicSymbolMap, diag.createAddendum())
);
});
return isKnown;
break;
}
case TypeCategory.OverloadedFunction: {
for (const overload of type.overloads) {
if (!this._isTypeKnown(report, overload, publicSymbolMap, diag.createAddendum())) {
isKnown = false;
}
knownStatus = this._updateKnownStatusIfWorse(
knownStatus,
this._getTypeKnownStatus(report, overload, publicSymbolMap, diag.createAddendum())
);
}
return isKnown;
break;
}
case TypeCategory.Function: {
if (!this._shouldIgnoreType(report, type.details.fullName)) {
if (
!this._validateFunctionType(
knownStatus = this._updateKnownStatusIfWorse(
knownStatus,
this._getFunctionTypeKnownStatus(
report,
type,
publicSymbolMap,
@ -1129,12 +1260,10 @@ export class PackageTypeVerifier {
/* declFilePath */ undefined,
diag
)
) {
isKnown = false;
}
);
}
return isKnown;
break;
}
case TypeCategory.Class: {
@ -1153,30 +1282,30 @@ export class PackageTypeVerifier {
diag.addMessage(
`Type argument ${index + 1} for class "${type.details.name}" has unknown type`
);
isKnown = false;
knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.Unknown);
} else if (isPartlyUnknown(typeArg)) {
diag.addMessage(
`Type argument ${index + 1} for class "${type.details.name}" has partially unknown type`
);
isKnown = false;
knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.PartiallyUnknown);
}
});
}
return isKnown;
break;
}
case TypeCategory.Module: {
if (!this._shouldIgnoreType(report, type.moduleName)) {
const moduleSymbol = this._getSymbolForModule(report, type, publicSymbolMap);
if (moduleSymbol.typeKnownStatus !== TypeKnownStatus.Known) {
isKnown = false;
}
knownStatus = this._updateKnownStatusIfWorse(knownStatus, moduleSymbol.typeKnownStatus);
}
return isKnown;
break;
}
}
return knownStatus;
}
private _getSymbolCategory(symbol: Symbol, type: Type): SymbolCategory {
@ -1305,4 +1434,9 @@ export class PackageTypeVerifier {
filePath: declFilePath,
});
}
private _updateKnownStatusIfWorse(currentStatus: TypeKnownStatus, newStatus: TypeKnownStatus) {
// Is the current status worse than the current status.
return newStatus > currentStatus ? newStatus : currentStatus;
}
}

View File

@ -53,6 +53,7 @@ interface PyrightJsonResults {
interface PyrightSymbolCount {
withKnownType: number;
withAmbiguousType: number;
withUnknownType: number;
}
@ -80,6 +81,7 @@ interface PyrightPublicSymbolReport {
name: string;
referenceCount: number;
isTypeKnown: boolean;
isTypeAmbiguous: boolean;
isExported: boolean;
diagnostics: PyrightJsonDiagnostic[];
alternateNames?: string[] | undefined;
@ -446,10 +448,12 @@ function buildTypeCompletenessReport(packageName: string, completenessReport: Pa
pyTypedPath: completenessReport.pyTypedPath,
exportedSymbolCounts: {
withKnownType: 0,
withAmbiguousType: 0,
withUnknownType: 0,
},
otherSymbolCounts: {
withKnownType: 0,
withAmbiguousType: 0,
withUnknownType: 0,
},
missingFunctionDocStringCount: completenessReport.missingFunctionDocStringCount,
@ -477,6 +481,7 @@ function buildTypeCompletenessReport(packageName: string, completenessReport: Pa
referenceCount: symbol.referenceCount,
isExported: symbol.isExported,
isTypeKnown: symbol.typeKnownStatus === TypeKnownStatus.Known,
isTypeAmbiguous: symbol.typeKnownStatus === TypeKnownStatus.Ambiguous,
diagnostics: symbol.diagnostics.map((diag) => convertDiagnosticToJson(diag.filePath, diag.diagnostic)),
};
@ -494,6 +499,12 @@ function buildTypeCompletenessReport(packageName: string, completenessReport: Pa
} else {
report.typeCompleteness!.otherSymbolCounts.withKnownType++;
}
} else if (symbol.typeKnownStatus === TypeKnownStatus.Ambiguous) {
if (symbol.isExported) {
report.typeCompleteness!.exportedSymbolCounts.withAmbiguousType++;
} else {
report.typeCompleteness!.otherSymbolCounts.withAmbiguousType++;
}
} else {
if (symbol.isExported) {
report.typeCompleteness!.exportedSymbolCounts.withUnknownType++;
@ -504,8 +515,10 @@ function buildTypeCompletenessReport(packageName: string, completenessReport: Pa
});
const unknownSymbolCount = report.typeCompleteness.exportedSymbolCounts.withUnknownType;
const ambiguousSymbolCount = report.typeCompleteness.exportedSymbolCounts.withAmbiguousType;
const knownSymbolCount = report.typeCompleteness.exportedSymbolCounts.withKnownType;
const totalSymbolCount = unknownSymbolCount + knownSymbolCount;
const totalSymbolCount = unknownSymbolCount + ambiguousSymbolCount + knownSymbolCount;
if (totalSymbolCount > 0) {
report.typeCompleteness!.completenessScore = knownSymbolCount / totalSymbolCount;
}
@ -537,7 +550,7 @@ function printTypeCompletenessReportText(results: PyrightJsonResults, verboseOut
// Print list of all symbols.
if (completenessReport.symbols.length > 0 && verboseOutput) {
console.log('');
console.log(`Exported symbols: ${completenessReport.symbols.length}`);
console.log(`Exported symbols: ${completenessReport.symbols.filter((sym) => sym.isExported).length}`);
completenessReport.symbols.forEach((symbol) => {
if (symbol.isExported) {
const refCount = symbol.referenceCount > 1 ? ` (${symbol.referenceCount} references)` : '';
@ -546,7 +559,7 @@ function printTypeCompletenessReportText(results: PyrightJsonResults, verboseOut
});
console.log('');
console.log(`Other referenced symbols: ${completenessReport.symbols.length}`);
console.log(`Other referenced symbols: ${completenessReport.symbols.filter((sym) => !sym.isExported).length}`);
completenessReport.symbols.forEach((symbol) => {
if (!symbol.isExported) {
const refCount = symbol.referenceCount > 1 ? ` (${symbol.referenceCount} references)` : '';
@ -581,11 +594,13 @@ function printTypeCompletenessReportText(results: PyrightJsonResults, verboseOut
console.log(
`Symbols exported by "${completenessReport.packageName}": ${
completenessReport.exportedSymbolCounts.withKnownType +
completenessReport.exportedSymbolCounts.withAmbiguousType +
completenessReport.exportedSymbolCounts.withUnknownType
}`
);
console.log(` With known type: ${completenessReport.exportedSymbolCounts.withKnownType}`);
console.log(` With partially unknown type: ${completenessReport.exportedSymbolCounts.withUnknownType}`);
console.log(` With ambiguous type: ${completenessReport.exportedSymbolCounts.withAmbiguousType}`);
console.log(` With unknown type: ${completenessReport.exportedSymbolCounts.withUnknownType}`);
if (completenessReport.ignoreUnknownTypesFromImports) {
console.log(` (Ignoring unknown types imported from other packages)`);
}
@ -595,11 +610,14 @@ function printTypeCompletenessReportText(results: PyrightJsonResults, verboseOut
console.log('');
console.log(
`Other symbols referenced but not exported by "${completenessReport.packageName}": ${
completenessReport.otherSymbolCounts.withKnownType + completenessReport.otherSymbolCounts.withUnknownType
completenessReport.otherSymbolCounts.withKnownType +
completenessReport.otherSymbolCounts.withAmbiguousType +
completenessReport.otherSymbolCounts.withUnknownType
}`
);
console.log(` With known type: ${completenessReport.otherSymbolCounts.withKnownType}`);
console.log(` With partially unknown type: ${completenessReport.otherSymbolCounts.withUnknownType}`);
console.log(` With ambiguous type: ${completenessReport.otherSymbolCounts.withAmbiguousType}`);
console.log(` With unknown type: ${completenessReport.otherSymbolCounts.withUnknownType}`);
console.log('');
console.log(`Type completeness score: ${Math.round(completenessReport.completenessScore * 1000) / 10}%`);
console.log('');