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 { AnalyzerFileInfo } from './analyzerFileInfo';
import * as AnalyzerNodeInfo from './analyzerNodeInfo'; import * as AnalyzerNodeInfo from './analyzerNodeInfo';
import { getBoundCallMethod, getBoundInitMethod, getBoundNewMethod } from './constructors'; import { getBoundCallMethod, getBoundInitMethod, getBoundNewMethod } from './constructors';
import { addInheritedDataClassEntries } from './dataClasses';
import { Declaration, DeclarationType, isAliasDeclaration } from './declaration'; import { Declaration, DeclarationType, isAliasDeclaration } from './declaration';
import { getNameNodeForDeclaration } from './declarationUtils'; import { getNameNodeForDeclaration } from './declarationUtils';
import { deprecatedAliases, deprecatedSpecialForms } from './deprecatedSymbols'; import { deprecatedAliases, deprecatedSpecialForms } from './deprecatedSymbols';
@ -165,6 +166,7 @@ import {
AnyType, AnyType,
ClassType, ClassType,
ClassTypeFlags, ClassTypeFlags,
DataClassEntry,
EnumLiteral, EnumLiteral,
FunctionType, FunctionType,
FunctionTypeFlags, FunctionTypeFlags,
@ -5294,6 +5296,13 @@ export class Checker extends ParseTreeWalker {
getProtocolSymbolsRecursive(classType, abstractSymbols, ClassTypeFlags.SupportsAbstractMethods); 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) => { ClassType.getSymbolTable(classType).forEach((localSymbol, name) => {
abstractSymbols.delete(name); abstractSymbols.delete(name);
@ -5376,20 +5385,30 @@ export class Checker extends ParseTreeWalker {
return; return;
} }
if (decls[0].type === DeclarationType.Variable) { if (decls[0].type !== DeclarationType.Variable) {
// If none of the declarations involve assignments, assume it's return;
// 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 // Dataclass fields are typically exempted from this check because
// protocol base class. Make sure it's implemented in the derived class. // they have synthesized __init__ methods that initialize these variables.
diagAddendum.addMessage( const dcEntry = dataClassEntries?.find((entry) => entry.name === name);
LocAddendum.uninitializedAbstractVariable().format({ if (dcEntry) {
name, if (dcEntry.includeInInit) {
classType: member.classType.details.name, 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()) { 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 // 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 // order. Returns true if all of the class types in the hierarchy are
// known, false if one or more are unknown. // known, false if one or more are unknown.
function addInheritedDataClassEntries(classType: ClassType, entries: DataClassEntry[]) { export function addInheritedDataClassEntries(classType: ClassType, entries: DataClassEntry[]) {
let allAncestorsAreKnown = true; let allAncestorsAreKnown = true;
ClassType.getReverseMro(classType).forEach((mroClass) => { ClassType.getReverseMro(classType).forEach((mroClass) => {

View File

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

View File

@ -2,7 +2,8 @@
# to a concrete implementation of an abstract base class that defines # to a concrete implementation of an abstract base class that defines
# (but does not assign) variables. # (but does not assign) variables.
from abc import ABC from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import NamedTuple, final from typing import NamedTuple, final
@ -52,3 +53,16 @@ class G(Abstract3):
class H(NamedTuple): class H(NamedTuple):
x: int 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