Implemented support for deprecated code hints. Also implemented code to detect the use of PEP 585-deprecated types, but this check is disabled for the time being because we think it will be too noisy and annoying for most users.

This commit is contained in:
Eric Traut 2021-09-28 19:57:03 -07:00
commit f4b5224a7c
14 changed files with 205 additions and 6 deletions

View File

@ -17,6 +17,7 @@ import { DiagnosticLevel } from '../common/configOptions';
import { assert } from '../common/debug';
import { Diagnostic, DiagnosticAddendum } from '../common/diagnostic';
import { DiagnosticRule } from '../common/diagnosticRules';
import { PythonVersion, versionToString } from '../common/pythonVersion';
import { TextRange } from '../common/textRange';
import { Localizer } from '../localization/localize';
import {
@ -150,6 +151,51 @@ interface LocalTypeVarInfo {
nodes: NameNode[];
}
interface DeprecatedForm {
version: PythonVersion;
fullName: string;
replacementText: string;
}
const deprecatedAliases = new Map<string, DeprecatedForm>([
['Tuple', { version: PythonVersion.V3_9, fullName: 'builtins.tuple', replacementText: 'tuple' }],
['List', { version: PythonVersion.V3_9, fullName: 'builtins.list', replacementText: 'list' }],
['Dict', { version: PythonVersion.V3_9, fullName: 'builtins.dict', replacementText: 'dict' }],
['Set', { version: PythonVersion.V3_9, fullName: 'builtins.set', replacementText: 'set' }],
['FrozenSet', { version: PythonVersion.V3_9, fullName: 'builtins.frozenset', replacementText: 'frozenset' }],
['Type', { version: PythonVersion.V3_9, fullName: 'builtins.type', replacementText: 'type' }],
['Deque', { version: PythonVersion.V3_9, fullName: 'collections.deque', replacementText: 'collections.deque' }],
[
'DefaultDict',
{
version: PythonVersion.V3_9,
fullName: 'collections.defaultdict',
replacementText: 'collections.defaultdict',
},
],
[
'OrderedDict',
{
version: PythonVersion.V3_9,
fullName: 'collections.OrderedDict',
replacementText: 'collections.OrderedDict',
},
],
[
'Counter',
{ version: PythonVersion.V3_9, fullName: 'collections.Counter', replacementText: 'collections.Counter' },
],
[
'ChainMap',
{ version: PythonVersion.V3_9, fullName: 'collections.ChainMap', replacementText: 'collections.ChainMap' },
],
]);
const deprecatedSpecialForms = new Map<string, DeprecatedForm>([
['Optional', { version: PythonVersion.V3_10, fullName: 'typing.Optional', replacementText: '| None' }],
['Union', { version: PythonVersion.V3_10, fullName: 'typing.Union', replacementText: '|' }],
]);
export class Checker extends ParseTreeWalker {
private readonly _moduleNode: ModuleNode;
private readonly _fileInfo: AnalyzerFileInfo;
@ -1049,6 +1095,11 @@ export class Checker extends ParseTreeWalker {
override visitName(node: NameNode) {
// Determine if we should log information about private usage.
this._conditionallyReportPrivateUsage(node);
// Report the use of a deprecated symbol. For now, this functionality
// is disabled. We'll leave it in place for the future.
// this._reportDeprecatedUse(node);
return true;
}
@ -2569,6 +2620,34 @@ export class Checker extends ParseTreeWalker {
return false;
}
private _reportDeprecatedUse(node: NameNode) {
const deprecatedForm = deprecatedAliases.get(node.value) ?? deprecatedSpecialForms.get(node.value);
if (!deprecatedForm) {
return;
}
const type = this._evaluator.getType(node);
if (!type) {
return;
}
if (!isInstantiableClass(type) || type.details.fullName !== deprecatedForm.fullName) {
return;
}
if (this._fileInfo.executionEnvironment.pythonVersion >= deprecatedForm.version) {
this._evaluator.addDeprecated(
Localizer.Diagnostic.deprecatedType().format({
version: versionToString(deprecatedForm.version),
replacement: deprecatedForm.replacementText,
}),
node
);
}
}
private _conditionallyReportPrivateUsage(node: NameNode) {
if (this._fileInfo.diagnosticRuleSet.reportPrivateUsage === 'none') {
return;

View File

@ -260,7 +260,7 @@ export class SourceFile {
if (options.diagnosticRuleSet.enableTypeIgnoreComments) {
if (Object.keys(this._typeIgnoreLines).length > 0) {
diagList = diagList.filter((d) => {
if (d.category !== DiagnosticCategory.UnusedCode) {
if (d.category !== DiagnosticCategory.UnusedCode && d.category !== DiagnosticCategory.Deprecated) {
for (let line = d.range.start.line; line <= d.range.end.line; line++) {
if (this._typeIgnoreLines[line]) {
return false;
@ -317,9 +317,12 @@ export class SourceFile {
// If we're not returning any diagnostics, filter out all of
// the errors and warnings, leaving only the unreachable code
// diagnostics.
// and deprecated diagnostics.
if (!includeWarningsAndErrors) {
diagList = diagList.filter((diag) => diag.category === DiagnosticCategory.UnusedCode);
diagList = diagList.filter(
(diag) =>
diag.category === DiagnosticCategory.UnusedCode || diag.category === DiagnosticCategory.Deprecated
);
}
return diagList;

View File

@ -2094,6 +2094,13 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
}
}
function addDeprecated(message: string, node: ParseNode) {
if (!isDiagnosticSuppressedForNode(node)) {
const fileInfo = AnalyzerNodeInfo.getFileInfo(node);
fileInfo.diagnosticSink.addDeprecatedWithTextRange(message, node);
}
}
function addDiagnosticWithSuppressionCheck(
diagLevel: DiagnosticLevel,
message: string,
@ -21138,6 +21145,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
addWarning,
addInformation,
addUnusedCode,
addDeprecated,
addDiagnostic,
addDiagnosticForTextRange,
printType,

View File

@ -336,6 +336,7 @@ export interface TypeEvaluator {
addWarning: (message: string, node: ParseNode) => Diagnostic | undefined;
addInformation: (message: string, node: ParseNode) => Diagnostic | undefined;
addUnusedCode: (node: ParseNode, textRange: TextRange) => void;
addDeprecated: (message: string, node: ParseNode) => void;
addDiagnostic: (
diagLevel: DiagnosticLevel,

View File

@ -130,6 +130,7 @@ export function createTypeEvaluatorWithTracker(
addWarning: (m, n) => run('addWarning', () => typeEvaluator.addWarning(m, n), n),
addInformation: (m, n) => run('addInformation', () => typeEvaluator.addInformation(m, n), n),
addUnusedCode: (n, t) => run('addUnusedCode', () => typeEvaluator.addUnusedCode(n, t), n),
addDeprecated: (m, n) => run('addDeprecated', () => typeEvaluator.addDeprecated(m, n), n),
addDiagnostic: (d, r, m, n) => run('addDiagnostic', () => typeEvaluator.addDiagnostic(d, r, m, n), n),
addDiagnosticForTextRange: (f, d, r, m, g) =>
run('addDiagnosticForTextRange', () => typeEvaluator.addDiagnosticForTextRange(f, d, r, m, g)),

View File

@ -20,6 +20,7 @@ export const enum DiagnosticCategory {
Warning,
Information,
UnusedCode,
Deprecated,
}
export function convertLevelToCategory(level: DiagnosticLevel) {

View File

@ -58,6 +58,14 @@ export class DiagnosticSink {
return this.addDiagnostic(diag);
}
addDeprecated(message: string, range: Range, action?: DiagnosticAction) {
const diag = new Diagnostic(DiagnosticCategory.Deprecated, message, range);
if (action) {
diag.addAction(action);
}
return this.addDiagnostic(diag);
}
addDiagnostic(diag: Diagnostic) {
// Create a unique key for the diagnostic to prevent
// adding duplicates.
@ -90,6 +98,10 @@ export class DiagnosticSink {
getUnusedCode() {
return this._diagnosticList.filter((diag) => diag.category === DiagnosticCategory.UnusedCode);
}
getDeprecated() {
return this._diagnosticList.filter((diag) => diag.category === DiagnosticCategory.Deprecated);
}
}
// Specialized version of DiagnosticSink that works with TextRange objects
@ -126,4 +138,12 @@ export class TextRangeDiagnosticSink extends DiagnosticSink {
action
);
}
addDeprecatedWithTextRange(message: string, range: TextRange, action?: DiagnosticAction) {
return this.addDeprecated(
message,
convertOffsetsToRange(range.start, range.start + range.length, this._lines),
action
);
}
}

View File

@ -164,6 +164,7 @@ interface ClientCapabilities {
completionDocFormat: MarkupKind;
completionSupportsSnippet: boolean;
signatureDocFormat: MarkupKind;
supportsDeprecatedDiagnosticTag: boolean;
supportsUnnecessaryDiagnosticTag: boolean;
completionItemResolveSupportsAdditionalTextEdits: boolean;
}
@ -187,6 +188,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
completionDocFormat: MarkupKind.PlainText,
completionSupportsSnippet: false,
signatureDocFormat: MarkupKind.PlainText,
supportsDeprecatedDiagnosticTag: false,
supportsUnnecessaryDiagnosticTag: false,
completionItemResolveSupportsAdditionalTextEdits: false,
};
@ -991,6 +993,9 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
this.client.supportsUnnecessaryDiagnosticTag = supportedDiagnosticTags.some(
(tag) => tag === DiagnosticTag.Unnecessary
);
this.client.supportsDeprecatedDiagnosticTag = supportedDiagnosticTags.some(
(tag) => tag === DiagnosticTag.Deprecated
);
this.client.hasWindowProgressCapability = !!capabilities.window?.workDoneProgress;
this.client.hasGoToDeclarationCapability = !!capabilities.textDocument?.declaration;
this.client.completionItemResolveSupportsAdditionalTextEdits =
@ -1237,6 +1242,14 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
if (!this.client.supportsUnnecessaryDiagnosticTag) {
return;
}
} else if (diag.category === DiagnosticCategory.Deprecated) {
vsDiag.tags = [DiagnosticTag.Deprecated];
vsDiag.severity = DiagnosticSeverity.Hint;
// If the client doesn't support "deprecated" tags, don't report.
if (!this.client.supportsDeprecatedDiagnosticTag) {
return;
}
}
if (rule) {
@ -1267,11 +1280,15 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
switch (category) {
case DiagnosticCategory.Error:
return DiagnosticSeverity.Error;
case DiagnosticCategory.Warning:
return DiagnosticSeverity.Warning;
case DiagnosticCategory.Information:
return DiagnosticSeverity.Information;
case DiagnosticCategory.UnusedCode:
case DiagnosticCategory.Deprecated:
return DiagnosticSeverity.Hint;
}
}

View File

@ -277,6 +277,10 @@ export namespace Localizer {
export const defaultValueContainsCall = () => getRawString('Diagnostic.defaultValueContainsCall');
export const defaultValueNotAllowed = () => getRawString('Diagnostic.defaultValueNotAllowed');
export const defaultValueNotEllipsis = () => getRawString('Diagnostic.defaultValueNotEllipsis');
export const deprecatedType = () =>
new ParameterizedString<{ version: string; replacement: string }>(
getRawString('Diagnostic.deprecatedType')
);
export const dictExpandIllegalInComprehension = () =>
getRawString('Diagnostic.dictExpandIllegalInComprehension');
export const dictInAnnotation = () => getRawString('Diagnostic.dictInAnnotation');

View File

@ -65,6 +65,7 @@
"defaultValueContainsCall": "Function calls and mutable objects not allowed within parameter default value expression",
"defaultValueNotAllowed": "Parameter with \"*\" or \"**\" cannot have default value",
"defaultValueNotEllipsis": "Default values in stub files should be specified as \"...\"",
"deprecatedType": "This type is deprecated as of Python {version}; use \"{replacement}\" instead",
"delTargetExpr": "Expression cannot be deleted",
"dictExpandIllegalInComprehension": "Dictionary expansion not allowed in comprehension",
"dictInAnnotation": "Dictionary expression not allowed in type annotation",

View File

@ -677,9 +677,9 @@ function reportDiagnosticsAsText(fileDiagnostics: FileDiagnostics[]): Diagnostic
let informationCount = 0;
fileDiagnostics.forEach((fileDiagnostics) => {
// Don't report unused code diagnostics.
// Don't report unused code or deprecated diagnostics.
const fileErrorsAndWarnings = fileDiagnostics.diagnostics.filter(
(diag) => diag.category !== DiagnosticCategory.UnusedCode
(diag) => diag.category !== DiagnosticCategory.UnusedCode && diag.category !== DiagnosticCategory.Deprecated
);
if (fileErrorsAndWarnings.length > 0) {

View File

@ -314,3 +314,21 @@ test('DuplicateDeclaration2', () => {
TestUtils.validateResults(analysisResults, 4);
});
// For now, this functionality is disabled.
// test('Deprecated1', () => {
// const configOptions = new ConfigOptions('.');
// configOptions.defaultPythonVersion = PythonVersion.V3_8;
// const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated1.py'], configOptions);
// TestUtils.validateResults(analysisResults1, 0, 0, 0, 0, 0);
// configOptions.defaultPythonVersion = PythonVersion.V3_9;
// const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['deprecated1.py'], configOptions);
// TestUtils.validateResults(analysisResults2, 0, 0, 0, 0, 11);
// configOptions.defaultPythonVersion = PythonVersion.V3_10;
// const analysisResults3 = TestUtils.typeAnalyzeSampleFiles(['deprecated1.py'], configOptions);
// TestUtils.validateResults(analysisResults3, 0, 0, 0, 0, 13);
// });

View File

@ -0,0 +1,37 @@
# This sample tests the detection of deprecated classes from the typing
# module.
from typing import (
ChainMap,
Counter,
DefaultDict,
Deque,
Dict,
FrozenSet,
List,
Optional,
OrderedDict,
Set,
Tuple,
Type,
Union,
)
# These should be marked deprecated for Python >= 3.9
v1: List[int] = [1, 2, 3]
v2: Dict[int, str] = {}
v3: Set[int] = set()
v4: Tuple[int] = (3,)
v5: FrozenSet[int] = frozenset()
v6: Type[int] = int
v7 = Deque()
v8 = DefaultDict()
v9 = OrderedDict()
v10 = Counter()
v11 = ChainMap()
# These should be marked deprecated for Python >= 3.10
v20: Union[int, str]
v21: Optional[int]

View File

@ -38,6 +38,7 @@ export interface FileAnalysisResult {
warnings: Diagnostic[];
infos: Diagnostic[];
unusedCodes: Diagnostic[];
deprecateds: Diagnostic[];
}
export interface FileParseResult {
@ -143,6 +144,7 @@ export function bindSampleFile(fileName: string, configOptions = new ConfigOptio
warnings: fileInfo.diagnosticSink.getWarnings(),
infos: fileInfo.diagnosticSink.getInformation(),
unusedCodes: fileInfo.diagnosticSink.getUnusedCode(),
deprecateds: fileInfo.diagnosticSink.getDeprecated(),
};
}
@ -184,6 +186,7 @@ export function typeAnalyzeSampleFiles(
warnings: diagnostics.filter((diag) => diag.category === DiagnosticCategory.Warning),
infos: diagnostics.filter((diag) => diag.category === DiagnosticCategory.Information),
unusedCodes: diagnostics.filter((diag) => diag.category === DiagnosticCategory.UnusedCode),
deprecateds: diagnostics.filter((diag) => diag.category === DiagnosticCategory.Deprecated),
};
return analysisResult;
} else {
@ -196,6 +199,7 @@ export function typeAnalyzeSampleFiles(
warnings: [],
infos: [],
unusedCodes: [],
deprecateds: [],
};
return analysisResult;
}
@ -223,7 +227,8 @@ export function validateResults(
errorCount: number,
warningCount = 0,
infoCount?: number,
unusedCode?: number
unusedCode?: number,
deprecated?: number
) {
assert.strictEqual(results.length, 1);
assert.strictEqual(results[0].errors.length, errorCount);
@ -236,4 +241,8 @@ export function validateResults(
if (unusedCode !== undefined) {
assert.strictEqual(results[0].unusedCodes.length, unusedCode);
}
if (deprecated !== undefined) {
assert.strictEqual(results[0].deprecateds.length, deprecated);
}
}