Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions packages/pyright-internal/src/analyzer/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5281,11 +5281,8 @@ export class Checker extends ParseTreeWalker {
} else {
const baseClasses = classType.shared.baseClasses.filter(isClass);
if (
(classType.shared.declaredMetaclass?.category !== TypeCategory.Class ||
!ClassType.isBuiltIn(classType.shared.declaredMetaclass, 'ABCMeta')) &&
!baseClasses.some(
(baseClass) => baseClass.shared.fullName === 'abc.ABC' || ClassType.isBuiltIn(baseClass, 'Protocol')
)
!ClassType.derivesFromExplicitAbstract(classType) &&
!baseClasses.some((baseClass) => ClassType.isBuiltIn(baseClass, 'Protocol'))
) {
const errorMessage = classType.shared.mro.some(
(baseClass) => isClass(baseClass) && ClassType.isBuiltIn(baseClass, 'Protocol')
Expand Down
73 changes: 42 additions & 31 deletions packages/pyright-internal/src/analyzer/typeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10786,43 +10786,54 @@ export function createTypeEvaluator(

if (ClassType.supportsAbstractMethods(expandedCallType)) {
const abstractSymbols = getAbstractSymbols(expandedCallType);
const derivesFromExplicitAbstract = ClassType.derivesFromExplicitAbstract(expandedCallType);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this variable is only used in one spot and should be moved inside the if statement where its used so its never checked unnecesarily


// check for abstract usages.
// only report an error if it's an exact type (ie. we know it's definitely not a subclass/impl)
if (!expandedCallType.priv.includeSubclasses && !isTypeVar(unexpandedCallType)) {
// Handle abstract classes with abstract methods (reportAbstractUsage)
if (abstractSymbols.length > 0) {
// If the class is abstract, it can't be instantiated.
const diagAddendum = new DiagnosticAddendum();
const errorsToDisplay = 2;

if (
abstractSymbols.length > 0 &&
!expandedCallType.priv.includeSubclasses &&
!isTypeVar(unexpandedCallType)
) {
// If the class is abstract, it can't be instantiated.
const diagAddendum = new DiagnosticAddendum();
const errorsToDisplay = 2;

abstractSymbols.forEach((abstractMethod, index) => {
if (index === errorsToDisplay) {
diagAddendum.addMessage(
LocAddendum.memberIsAbstractMore().format({
count: abstractSymbols.length - errorsToDisplay,
})
);
} else if (index < errorsToDisplay) {
if (isInstantiableClass(abstractMethod.classType)) {
const className = abstractMethod.classType.shared.name;
abstractSymbols.forEach((abstractMethod, index) => {
if (index === errorsToDisplay) {
diagAddendum.addMessage(
LocAddendum.memberIsAbstract().format({
type: className,
name: abstractMethod.symbolName,
LocAddendum.memberIsAbstractMore().format({
count: abstractSymbols.length - errorsToDisplay,
})
);
} else if (index < errorsToDisplay) {
if (isInstantiableClass(abstractMethod.classType)) {
const className = abstractMethod.classType.shared.name;
diagAddendum.addMessage(
LocAddendum.memberIsAbstract().format({
type: className,
name: abstractMethod.symbolName,
})
);
}
}
}
});
});

addDiagnostic(
DiagnosticRule.reportAbstractUsage,
LocMessage.instantiateAbstract().format({
type: expandedCallType.shared.name,
}) + diagAddendum.getString(),
errorNode
);
addDiagnostic(
DiagnosticRule.reportAbstractUsage,
LocMessage.instantiateAbstract().format({
type: expandedCallType.shared.name,
}) + diagAddendum.getString(),
errorNode
);
} else if (derivesFromExplicitAbstract) {
// Handle abstract classes with no abstract methods (reportEmptyAbstractClass)
addDiagnostic(
DiagnosticRule.reportExplicitAbstractUsage,
LocMessage.instantiateAbstract().format({
type: expandedCallType.shared.name,
}) + LocAddendum.classIsExplicitlyAbstract().getFormatString(),
errorNode
);
}
}
}

Expand Down
14 changes: 14 additions & 0 deletions packages/pyright-internal/src/analyzer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,20 @@ export namespace ClassType {
return !!(classType.shared.flags & ClassTypeFlags.SupportsAbstractMethods);
}

export function derivesFromExplicitAbstract(classType: ClassType): boolean {
Copy link
Owner

@DetachHead DetachHead Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this name seems misleading. maybe isDirectSubtypeOfAbstractClass or something. or add a docstyring that explains it means direct subtype

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a doc string? what on earth is that?????

// Check if ABC is in the direct base classes
const derivesDirectlyFromABC = classType.shared.baseClasses.some(
(baseClass) => isClass(baseClass) && baseClass.shared.fullName === 'abc.ABC'
);
// Check if the class uses ABCMeta as its metaclass
const hasABCMetaMetaclass =
classType.shared.declaredMetaclass !== undefined &&
isClass(classType.shared.declaredMetaclass) &&
classType.shared.declaredMetaclass.shared.fullName === 'abc.ABCMeta';
Comment on lines +1179 to +1187
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code is so incomprehensibly confusing if only it could be rewritten to be simple enough that the code was self documenting so that the comments arent needed

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true, i'll close the pr


return derivesDirectlyFromABC || hasABCMetaMetaclass;
}

export function isDataClass(classType: ClassType) {
return !!classType.shared.dataClassBehaviors;
}
Expand Down
10 changes: 10 additions & 0 deletions packages/pyright-internal/src/common/configOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ export interface DiagnosticRuleSet {
// Report use of abstract method or variable?
reportAbstractUsage: DiagnosticLevel;

// Report instantiation of abstract class with no abstract methods?
reportExplicitAbstractUsage: DiagnosticLevel;

// Report argument type incompatibilities?
reportArgumentType: DiagnosticLevel;

Expand Down Expand Up @@ -481,6 +484,7 @@ export function getDiagLevelDiagnosticRules() {
DiagnosticRule.reportDuplicateImport,
DiagnosticRule.reportWildcardImportFromLibrary,
DiagnosticRule.reportAbstractUsage,
DiagnosticRule.reportExplicitAbstractUsage,
DiagnosticRule.reportArgumentType,
DiagnosticRule.reportAssertTypeFailure,
DiagnosticRule.reportAssignmentType,
Expand Down Expand Up @@ -617,6 +621,7 @@ export function getOffDiagnosticRuleSet(): DiagnosticRuleSet {
reportDuplicateImport: 'none',
reportWildcardImportFromLibrary: 'none',
reportAbstractUsage: 'none',
reportExplicitAbstractUsage: 'none',
reportArgumentType: 'none',
reportAssertTypeFailure: 'none',
reportAssignmentType: 'none',
Expand Down Expand Up @@ -737,6 +742,7 @@ export function getBasicDiagnosticRuleSet(): DiagnosticRuleSet {
reportDuplicateImport: 'none',
reportWildcardImportFromLibrary: 'warning',
reportAbstractUsage: 'error',
reportExplicitAbstractUsage: 'error',
reportArgumentType: 'error',
reportAssertTypeFailure: 'error',
reportAssignmentType: 'error',
Expand Down Expand Up @@ -857,6 +863,7 @@ export function getStandardDiagnosticRuleSet(): DiagnosticRuleSet {
reportDuplicateImport: 'none',
reportWildcardImportFromLibrary: 'warning',
reportAbstractUsage: 'error',
reportExplicitAbstractUsage: 'error',
reportArgumentType: 'error',
reportAssertTypeFailure: 'error',
reportAssignmentType: 'error',
Expand Down Expand Up @@ -976,6 +983,7 @@ export const getRecommendedDiagnosticRuleSet = (): DiagnosticRuleSet => ({
reportDuplicateImport: 'warning',
reportWildcardImportFromLibrary: 'warning',
reportAbstractUsage: 'error',
reportExplicitAbstractUsage: 'error',
reportArgumentType: 'error',
reportAssertTypeFailure: 'error',
reportAssignmentType: 'error',
Expand Down Expand Up @@ -1092,6 +1100,7 @@ export const getAllDiagnosticRuleSet = (): DiagnosticRuleSet => ({
reportDuplicateImport: 'error',
reportWildcardImportFromLibrary: 'error',
reportAbstractUsage: 'error',
reportExplicitAbstractUsage: 'error',
reportArgumentType: 'error',
reportAssertTypeFailure: 'error',
reportAssignmentType: 'error',
Expand Down Expand Up @@ -1209,6 +1218,7 @@ export function getStrictDiagnosticRuleSet(): DiagnosticRuleSet {
reportDuplicateImport: 'error',
reportWildcardImportFromLibrary: 'error',
reportAbstractUsage: 'error',
reportExplicitAbstractUsage: 'error',
reportArgumentType: 'error',
reportAssertTypeFailure: 'error',
reportAssignmentType: 'error',
Expand Down
1 change: 1 addition & 0 deletions packages/pyright-internal/src/common/diagnosticRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export enum DiagnosticRule {
reportDuplicateImport = 'reportDuplicateImport',
reportWildcardImportFromLibrary = 'reportWildcardImportFromLibrary',
reportAbstractUsage = 'reportAbstractUsage',
reportExplicitAbstractUsage = 'reportExplicitAbstractUsage',
reportArgumentType = 'reportArgumentType',
reportAssertTypeFailure = 'reportAssertTypeFailure',
reportAssignmentType = 'reportAssignmentType',
Expand Down
2 changes: 2 additions & 0 deletions packages/pyright-internal/src/localization/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1393,6 +1393,8 @@ export namespace Localizer {
);
export const memberIsAbstractMore = () =>
new ParameterizedString<{ count: number }>(getRawString('DiagnosticAddendum.memberIsAbstractMore'));
export const classIsExplicitlyAbstract = () =>
new ParameterizedString<{ count: number }>(getRawString('DiagnosticAddendum.classIsExplicitlyAbstract'));
export const memberIsClassVarInProtocol = () =>
new ParameterizedString<{ name: string }>(getRawString('DiagnosticAddendum.memberIsClassVarInProtocol'));
export const memberIsInitVar = () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1934,6 +1934,9 @@
"message": "and {count} more...",
"comment": "{StrEnds='...'}"
},
"classIsExplicitlyAbstract": {
"message": "class has no abstract members, but is explicitly denoted with \"ABC\" or \"ABCMeta\""
},
"memberIsClassVarInProtocol": {
"message": "\"{name}\" is defined as a ClassVar in protocol",
"comment": "{Locked='ClassVar'}"
Expand Down
16 changes: 16 additions & 0 deletions packages/pyright-internal/src/tests/checker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,22 @@ test('AbstractClass11', () => {
TestUtils.validateResults(analysisResults, 2);
});

test('AbstractClass12', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['abstractClass12.py']);

TestUtils.validateResultsButBased(analysisResults, {
// one error to validate the message, the rest use `pyright: ignore`
errors: [
{
line: 11,
message:
'Cannot instantiate abstract class "ExplicitlyAbstract"class has no abstract members, but is explicitly denoted with "ABC" or "ABCMeta"',
code: DiagnosticRule.reportExplicitAbstractUsage,
},
],
});
});

test('Constants1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['constants1.py']);

Expand Down
40 changes: 40 additions & 0 deletions packages/pyright-internal/src/tests/samples/abstractClass12.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# this sample tests the type analyzer's ability to flag attempts
# to instantiate abstract base classes that have no abstract methods

from abc import ABC, ABCMeta


class ExplicitlyAbstract(ABC):
"""an abstract class with no abstract methods"""

# this should generate an error because `AbstractFoo`
# is an abstract class even though it has no abstract methods
a = ExplicitlyAbstract()

class IndirectlyAbstract(ExplicitlyAbstract):
"""inherits from an explicitly abstract class"""


# this should not generate an error because IndirectlyAbstract
# doesn't directly inherit from ABC (only through AbstractFoo)
Comment on lines +10 to +19
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AbstractFoo doesnt exist anymore

f = IndirectlyAbstract()
Comment on lines +14 to +20
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IndirectlyAbstract doesnt sound like the right name because its not supposed to be abstract



class NotAbstract:
"""a regular class that doesn't derive from ABC"""

# This should not generate an error because NotAbstract is not abstract
e = NotAbstract()

class AbstractWithMetaclass(metaclass=ABCMeta):
"""abstract class using ABCMeta metaclass with no abstract methods"""

class ConcreteWithMetaclass(AbstractWithMetaclass):
"""concrete subclass of AbstractWithMetaclass"""


# this should generate an error because AbstractWithMetaclass uses ABCMeta
i = AbstractWithMetaclass() # pyright: ignore[reportExplicitAbstractUsage]

# this should not generate an error
j = ConcreteWithMetaclass()
2 changes: 1 addition & 1 deletion packages/pyright-internal/src/tests/samples/async1.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ async def b():
yield i


cm = AsyncExitStack()
cm = AsyncExitStack() # pyright: ignore[reportExplicitAbstractUsage] # typeshed moment


def func1():
Expand Down
2 changes: 1 addition & 1 deletion packages/pyright-internal/src/tests/samples/property2.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def y(self) -> float:
raise NotImplementedError


a = Foo()
a = Foo() # pyright: ignore[reportExplicitAbstractUsage]
requires_int(a.x)

a.x = 3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ def __getattr__(self, attr):
return self.__getattribute__(attr)


b1 = ClassB("test")
b1 = ClassB("test") # pyright: ignore[reportExplicitAbstractUsage]
reveal_type(b1.value, expected_text="Unknown | Any | None")
del b1.cache
17 changes: 17 additions & 0 deletions packages/vscode-pyright/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,23 @@
false
]
},
"reportExplicitAbstractUsage": {
"type": [
"string",
"boolean"
],
"description": "Diagnostics for an attempt to instantiate an abstract class that has no abstract members.",
"default": "error",
"enum": [
"none",
"hint",
"information",
"warning",
"error",
true,
false
]
},
"reportArgumentType": {
"type": [
"string",
Expand Down
8 changes: 8 additions & 0 deletions packages/vscode-pyright/schemas/pyrightconfig.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@
"title": "Controls reporting of attempted instantiation of abstract class",
"default": "error"
},
"reportExplicitAbstractUsage": {
"$ref": "#/definitions/diagnostic",
"title": "Controls reporting of attempted instantiation of empty abstract class",
"default": "error"
},
"reportArgumentType": {
"$ref": "#/definitions/diagnostic",
"title": "Controls reporting of incompatible argument type",
Expand Down Expand Up @@ -744,6 +749,9 @@
"reportAbstractUsage": {
"$ref": "#/definitions/reportAbstractUsage"
},
"reportExplicitAbstractUsage": {
"$ref": "#/definitions/reportExplicitAbstractUsage"
},
"reportArgumentType": {
"$ref": "#/definitions/reportArgumentType"
},
Expand Down
Loading