mirror of
https://github.com/microsoft/pyright.git
synced 2024-10-07 13:29:17 +03:00
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:
commit
f4b5224a7c
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)),
|
||||
|
@ -20,6 +20,7 @@ export const enum DiagnosticCategory {
|
||||
Warning,
|
||||
Information,
|
||||
UnusedCode,
|
||||
Deprecated,
|
||||
}
|
||||
|
||||
export function convertLevelToCategory(level: DiagnosticLevel) {
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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",
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
// });
|
||||
|
37
packages/pyright-internal/src/tests/samples/deprecated1.py
Normal file
37
packages/pyright-internal/src/tests/samples/deprecated1.py
Normal 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]
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user