private _validateBaseClassOverrides()

in packages/pyright-internal/src/analyzer/checker.ts [4132:4476]


    private _validateBaseClassOverrides(classType: ClassType) {
        classType.details.fields.forEach((symbol, name) => {
            // Private symbols do not need to match in type since their
            // names are mangled, and subclasses can't access the value in
            // the parent class.
            if (SymbolNameUtils.isPrivateName(name)) {
                return;
            }

            // If the symbol has no declaration, and the type is inferred,
            // skip this check.
            if (!symbol.hasTypedDeclarations()) {
                return;
            }

            // Get the symbol type defined in this class.
            const typeOfSymbol = this._evaluator.getEffectiveTypeOfSymbol(symbol);

            // If the type of the override symbol isn't known, stop here.
            if (isAnyOrUnknown(typeOfSymbol)) {
                return;
            }

            for (const baseClass of classType.details.baseClasses) {
                if (!isClass(baseClass)) {
                    continue;
                }

                // Look up the base class in the MRO list. It's the same generic class
                // but has already been specialized using the type variables of the classType.
                const mroBaseClass = classType.details.mro.find(
                    (mroClass) => isClass(mroClass) && ClassType.isSameGenericClass(mroClass, baseClass)
                );
                if (!mroBaseClass) {
                    continue;
                }

                const baseClassAndSymbol = lookUpClassMember(mroBaseClass, name, ClassMemberLookupFlags.Default);

                if (!baseClassAndSymbol || !isInstantiableClass(baseClassAndSymbol.classType)) {
                    continue;
                }

                // If the base class doesn't provide a type declaration, we won't bother
                // proceeding with additional checks. Type inference is too inaccurate
                // in this case, plus it would be very slow.
                if (!baseClassAndSymbol.symbol.hasTypedDeclarations()) {
                    continue;
                }

                const baseClassSymbolType = partiallySpecializeType(
                    this._evaluator.getEffectiveTypeOfSymbol(baseClassAndSymbol.symbol),
                    baseClassAndSymbol.classType
                );

                if (isFunction(baseClassSymbolType) || isOverloadedFunction(baseClassSymbolType)) {
                    const diagAddendum = new DiagnosticAddendum();
                    let overrideFunction: FunctionType | undefined;

                    if (isFunction(typeOfSymbol)) {
                        overrideFunction = typeOfSymbol;
                    } else if (isOverloadedFunction(typeOfSymbol)) {
                        // Use the last overload.
                        overrideFunction = typeOfSymbol.overloads[typeOfSymbol.overloads.length - 1];
                    }

                    if (overrideFunction) {
                        const exemptMethods = ['__init__', '__new__', '__init_subclass__'];

                        // Don't enforce parameter names for dundered methods. Many of them
                        // are misnamed in typeshed stubs, so this would result in many
                        // false positives.
                        const enforceParamNameMatch = !SymbolNameUtils.isDunderName(name);

                        // Don't check certain magic functions or private symbols.
                        if (!exemptMethods.some((exempt) => exempt === name) && !SymbolNameUtils.isPrivateName(name)) {
                            if (
                                !this._evaluator.canOverrideMethod(
                                    baseClassSymbolType,
                                    overrideFunction,
                                    diagAddendum,
                                    enforceParamNameMatch
                                )
                            ) {
                                const decl =
                                    overrideFunction.details.declaration ?? getLastTypedDeclaredForSymbol(symbol);
                                if (decl) {
                                    const diag = this._evaluator.addDiagnostic(
                                        this._fileInfo.diagnosticRuleSet.reportIncompatibleMethodOverride,
                                        DiagnosticRule.reportIncompatibleMethodOverride,
                                        Localizer.Diagnostic.incompatibleMethodOverride().format({
                                            name,
                                            className: baseClassAndSymbol.classType.details.name,
                                        }) + diagAddendum.getString(),
                                        decl.type === DeclarationType.Function ? decl.node.name : decl.node
                                    );

                                    const origDecl = getLastTypedDeclaredForSymbol(baseClassAndSymbol.symbol);
                                    if (diag && origDecl) {
                                        diag.addRelatedInfo(
                                            Localizer.DiagnosticAddendum.overriddenMethod(),
                                            origDecl.path,
                                            origDecl.range
                                        );
                                    }
                                }
                            }
                        }

                        if (isFunction(baseClassSymbolType)) {
                            // Private names (starting with double underscore) are exempt from this check.
                            if (!SymbolNameUtils.isPrivateName(name) && FunctionType.isFinal(baseClassSymbolType)) {
                                const decl = getLastTypedDeclaredForSymbol(symbol);
                                if (decl && decl.type === DeclarationType.Function) {
                                    const diag = this._evaluator.addError(
                                        Localizer.Diagnostic.finalMethodOverride().format({
                                            name,
                                            className: baseClassAndSymbol.classType.details.name,
                                        }),
                                        decl.node.name
                                    );

                                    const origDecl = getLastTypedDeclaredForSymbol(baseClassAndSymbol.symbol);
                                    if (diag && origDecl) {
                                        diag.addRelatedInfo(
                                            Localizer.DiagnosticAddendum.finalMethod(),
                                            origDecl.path,
                                            origDecl.range
                                        );
                                    }
                                }
                            }
                        }
                    } else if (!isAnyOrUnknown(typeOfSymbol)) {
                        // Special-case overrides of methods in '_TypedDict', since
                        // TypedDict attributes aren't manifest as attributes but rather
                        // as named keys.
                        if (!ClassType.isBuiltIn(baseClassAndSymbol.classType, '_TypedDict')) {
                            const decls = symbol.getDeclarations();
                            if (decls.length > 0) {
                                const lastDecl = decls[decls.length - 1];
                                const diag = this._evaluator.addDiagnostic(
                                    this._fileInfo.diagnosticRuleSet.reportIncompatibleMethodOverride,
                                    DiagnosticRule.reportIncompatibleMethodOverride,
                                    Localizer.Diagnostic.methodOverridden().format({
                                        name,
                                        className: baseClassAndSymbol.classType.details.name,
                                        type: this._evaluator.printType(typeOfSymbol, /* expandTypeAlias */ false),
                                    }),
                                    lastDecl.node
                                );

                                const origDecl = getLastTypedDeclaredForSymbol(baseClassAndSymbol.symbol);
                                if (diag && origDecl) {
                                    diag.addRelatedInfo(
                                        Localizer.DiagnosticAddendum.overriddenMethod(),
                                        origDecl.path,
                                        origDecl.range
                                    );
                                }
                            }
                        }
                    }
                } else if (isProperty(baseClassSymbolType)) {
                    // Handle properties specially.
                    if (!isProperty(typeOfSymbol)) {
                        const decls = symbol.getDeclarations();
                        if (decls.length > 0) {
                            this._evaluator.addDiagnostic(
                                this._fileInfo.diagnosticRuleSet.reportIncompatibleMethodOverride,
                                DiagnosticRule.reportIncompatibleMethodOverride,
                                Localizer.Diagnostic.propertyOverridden().format({
                                    name,
                                    className: baseClassAndSymbol.classType.details.name,
                                }),
                                decls[decls.length - 1].node
                            );
                        }
                    } else {
                        const basePropFields = (baseClassSymbolType as ClassType).details.fields;
                        const subclassPropFields = (typeOfSymbol as ClassType).details.fields;
                        const baseClassType = baseClassAndSymbol.classType;

                        ['fget', 'fset', 'fdel'].forEach((methodName) => {
                            const diagAddendum = new DiagnosticAddendum();
                            const baseClassPropMethod = basePropFields.get(methodName);
                            const subclassPropMethod = subclassPropFields.get(methodName);

                            // Is the method present on the base class but missing in the subclass?
                            if (baseClassPropMethod) {
                                const baseClassMethodType = partiallySpecializeType(
                                    this._evaluator.getEffectiveTypeOfSymbol(baseClassPropMethod),
                                    baseClassType
                                );
                                if (isFunction(baseClassMethodType)) {
                                    if (!subclassPropMethod) {
                                        // The method is missing.
                                        diagAddendum.addMessage(
                                            Localizer.DiagnosticAddendum.propertyMethodMissing().format({
                                                name: methodName,
                                            })
                                        );
                                        const decls = symbol.getDeclarations();
                                        if (decls.length > 0) {
                                            const diag = this._evaluator.addDiagnostic(
                                                this._fileInfo.diagnosticRuleSet.reportIncompatibleMethodOverride,
                                                DiagnosticRule.reportIncompatibleMethodOverride,
                                                Localizer.Diagnostic.propertyOverridden().format({
                                                    name,
                                                    className: baseClassType.details.name,
                                                }) + diagAddendum.getString(),
                                                decls[decls.length - 1].node
                                            );

                                            const origDecl = baseClassMethodType.details.declaration;
                                            if (diag && origDecl) {
                                                diag.addRelatedInfo(
                                                    Localizer.DiagnosticAddendum.overriddenMethod(),
                                                    origDecl.path,
                                                    origDecl.range
                                                );
                                            }
                                        }
                                    } else {
                                        const subclassMethodType = partiallySpecializeType(
                                            this._evaluator.getEffectiveTypeOfSymbol(subclassPropMethod),
                                            classType
                                        );
                                        if (isFunction(subclassMethodType)) {
                                            if (
                                                !this._evaluator.canOverrideMethod(
                                                    baseClassMethodType,
                                                    subclassMethodType,
                                                    diagAddendum.createAddendum()
                                                )
                                            ) {
                                                diagAddendum.addMessage(
                                                    Localizer.DiagnosticAddendum.propertyMethodIncompatible().format({
                                                        name: methodName,
                                                    })
                                                );
                                                const decl = subclassMethodType.details.declaration;
                                                if (decl && decl.type === DeclarationType.Function) {
                                                    const diag = this._evaluator.addDiagnostic(
                                                        this._fileInfo.diagnosticRuleSet
                                                            .reportIncompatibleMethodOverride,
                                                        DiagnosticRule.reportIncompatibleMethodOverride,
                                                        Localizer.Diagnostic.propertyOverridden().format({
                                                            name,
                                                            className: baseClassType.details.name,
                                                        }) + diagAddendum.getString(),
                                                        decl.node.name
                                                    );

                                                    const origDecl = baseClassMethodType.details.declaration;
                                                    if (diag && origDecl) {
                                                        diag.addRelatedInfo(
                                                            Localizer.DiagnosticAddendum.overriddenMethod(),
                                                            origDecl.path,
                                                            origDecl.range
                                                        );
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        });
                    }
                } else {
                    // This check can be expensive, so don't perform it if the corresponding
                    // rule is disabled.
                    if (this._fileInfo.diagnosticRuleSet.reportIncompatibleVariableOverride !== 'none') {
                        const decls = symbol.getDeclarations();
                        if (decls.length > 0) {
                            const lastDecl = decls[decls.length - 1];
                            // Verify that the override type is assignable to (same or narrower than)
                            // the declared type of the base symbol.
                            const diagAddendum = new DiagnosticAddendum();
                            if (!this._evaluator.canAssignType(baseClassSymbolType, typeOfSymbol, diagAddendum)) {
                                const diag = this._evaluator.addDiagnostic(
                                    this._fileInfo.diagnosticRuleSet.reportIncompatibleVariableOverride,
                                    DiagnosticRule.reportIncompatibleVariableOverride,
                                    Localizer.Diagnostic.symbolOverridden().format({
                                        name,
                                        className: baseClassAndSymbol.classType.details.name,
                                    }) + diagAddendum.getString(),
                                    lastDecl.node
                                );

                                const origDecl = getLastTypedDeclaredForSymbol(baseClassAndSymbol.symbol);
                                if (diag && origDecl) {
                                    diag.addRelatedInfo(
                                        Localizer.DiagnosticAddendum.overriddenSymbol(),
                                        origDecl.path,
                                        origDecl.range
                                    );
                                }
                            }

                            // Verify that a class variable isn't overriding an instance
                            // variable or vice versa.
                            const isBaseClassVar = baseClassAndSymbol.symbol.isClassVar();
                            let isClassVar = symbol.isClassVar();

                            // If the subclass doesn't redeclare the type but simply assigns
                            // it without declaring its type, we won't consider it an instance
                            // variable.
                            if (isBaseClassVar && !isClassVar) {
                                if (!symbol.hasTypedDeclarations()) {
                                    isClassVar = true;
                                }
                            }

                            if (isBaseClassVar !== isClassVar) {
                                const unformattedMessage = symbol.isClassVar()
                                    ? Localizer.Diagnostic.classVarOverridesInstanceVar()
                                    : Localizer.Diagnostic.instanceVarOverridesClassVar();

                                const diag = this._evaluator.addDiagnostic(
                                    this._fileInfo.diagnosticRuleSet.reportIncompatibleVariableOverride,
                                    DiagnosticRule.reportIncompatibleVariableOverride,
                                    unformattedMessage.format({
                                        name,
                                        className: baseClassAndSymbol.classType.details.name,
                                    }),
                                    lastDecl.node
                                );

                                const origDecl = getLastTypedDeclaredForSymbol(baseClassAndSymbol.symbol);
                                if (diag && origDecl) {
                                    diag.addRelatedInfo(
                                        Localizer.DiagnosticAddendum.overriddenSymbol(),
                                        origDecl.path,
                                        origDecl.range
                                    );
                                }
                            }
                        }
                    }
                }
            }
        });
    }