Improved reportUninitializedInstanceVariable check to handle dataclass variables that are initialized implicitly by the synthesized __init__ method.

This commit is contained in:
Eric Traut 2024-06-22 21:44:36 +02:00
parent 3e6661a315
commit ee12ebcccf
4 changed files with 48 additions and 15 deletions

View File

@ -95,6 +95,7 @@ import { OperatorType, StringTokenFlags, TokenType } from '../parser/tokenizerTy
import { AnalyzerFileInfo } from './analyzerFileInfo';
import * as AnalyzerNodeInfo from './analyzerNodeInfo';
import { getBoundCallMethod, getBoundInitMethod, getBoundNewMethod } from './constructors';
import { addInheritedDataClassEntries } from './dataClasses';
import { Declaration, DeclarationType, isAliasDeclaration } from './declaration';
import { getNameNodeForDeclaration } from './declarationUtils';
import { deprecatedAliases, deprecatedSpecialForms } from './deprecatedSymbols';
@ -165,6 +166,7 @@ import {
AnyType,
ClassType,
ClassTypeFlags,
DataClassEntry,
EnumLiteral,
FunctionType,
FunctionTypeFlags,
@ -5294,6 +5296,13 @@ export class Checker extends ParseTreeWalker {
getProtocolSymbolsRecursive(classType, abstractSymbols, ClassTypeFlags.SupportsAbstractMethods);
}
// If this is a dataclass, get all of the entries so we can tell which
// ones are initialized by the synthesized __init__ method.
const dataClassEntries: DataClassEntry[] = [];
if (ClassType.isDataClass(classType)) {
addInheritedDataClassEntries(classType, dataClassEntries);
}
ClassType.getSymbolTable(classType).forEach((localSymbol, name) => {
abstractSymbols.delete(name);
@ -5376,20 +5385,30 @@ export class Checker extends ParseTreeWalker {
return;
}
if (decls[0].type === DeclarationType.Variable) {
// If none of the declarations involve assignments, assume it's
// not implemented in the protocol.
if (!decls.some((decl) => decl.type === DeclarationType.Variable && !!decl.inferredTypeSource)) {
// This is a variable declaration that is not implemented in the
// protocol base class. Make sure it's implemented in the derived class.
diagAddendum.addMessage(
LocAddendum.uninitializedAbstractVariable().format({
name,
classType: member.classType.details.name,
})
);
if (decls[0].type !== DeclarationType.Variable) {
return;
}
// Dataclass fields are typically exempted from this check because
// they have synthesized __init__ methods that initialize these variables.
const dcEntry = dataClassEntries?.find((entry) => entry.name === name);
if (dcEntry) {
if (dcEntry.includeInInit) {
return;
}
} else {
// Do one or more declarations involve assignments?
if (decls.some((decl) => decl.type === DeclarationType.Variable && !!decl.inferredTypeSource)) {
return;
}
}
diagAddendum.addMessage(
LocAddendum.uninitializedAbstractVariable().format({
name,
classType: member.classType.details.name,
})
);
});
if (!diagAddendum.isEmpty()) {

View File

@ -995,7 +995,7 @@ function transformDescriptorType(evaluator: TypeEvaluator, type: Type): Type {
// the specified class. These entries must be unique and in reverse-MRO
// order. Returns true if all of the class types in the hierarchy are
// known, false if one or more are unknown.
function addInheritedDataClassEntries(classType: ClassType, entries: DataClassEntry[]) {
export function addInheritedDataClassEntries(classType: ClassType, entries: DataClassEntry[]) {
let allAncestorsAreKnown = true;
ClassType.getReverseMro(classType).forEach((mroClass) => {

View File

@ -493,7 +493,7 @@ test('UninitializedVariable2', () => {
// Enable it as an error.
configOptions.diagnosticRuleSet.reportUninitializedInstanceVariable = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['uninitializedVariable2.py'], configOptions);
TestUtils.validateResults(analysisResults, 2);
TestUtils.validateResults(analysisResults, 3);
});
test('Deprecated1', () => {

View File

@ -2,7 +2,8 @@
# to a concrete implementation of an abstract base class that defines
# (but does not assign) variables.
from abc import ABC
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import NamedTuple, final
@ -52,3 +53,16 @@ class G(Abstract3):
class H(NamedTuple):
x: int
@dataclass
class IAbstract(ABC):
p1: str
p2: int = field(init=False)
@final
@dataclass
# This should generate an error because p2 is uninitialized.
class I(IAbstract):
p3: int