dart-lang / dart-lang/language

Make deprecated APIs expire based on language version.

Open
#1,075 8 comments 3 reactions 0 assignees View on GitHub
feature
Dominant language
TeX
Stars
2.9k
Forks
239
Avg merge
2d 18h
Merged PRs (30d)
14

Description

Currently the Dart `@Deprecated(...)` or `@deprecated` annotations only trigger warnings.
It's possible to say when the feature will be removed in the message, but it's still going to be a hard breaking change when that happens because existing code will stop working.

Deprecation is a two-step process: 1) Mark as deprecated and 2) remove entirely.

There is no gradually increase in motivation to remove usages of the deprecated member, it goes directly from "want to avoid warnings" to "program doesn't run". That makes some projects turn deprecation warnings into errors, which means that merely marking something as deprecated will break those projects. That loses the point of deprecating before removing: to *not* make it a sudden breaking change

We could introduce a third, intermediate, stage, "discontinued", where the API stops working for all code running above a certain language version. This is a kind of "library versioning", but since all it does is remove access, it's much simpler than having different members for different language versions.

In short: Add a language version field to `Deprecated`, and refuse access to the deprecated feature from code compiled at that language version or above. Example:
```dart
@Deprecated("Doesn't work any more", discontinued: "2.11")
void foo() { ... }
```
Code running at language version 2.11 or above will not be allowed to call `foo`. Code running at language version 2.10 or below can keep using it, and will still receive deprecation warnings.

This forces users to address the deprecation warning at a point where they are also addressing other changes required by the SDK, without breaking code that stays on the older version. It allows users to gradually migrate, while still enforcing an eventual discontinuation.
The feature can then be removed finally from the SDK when we stop supporting the older language versions, so for the above, when we stop supporting language version 2.10, we also remove the feature finally. (We can remove it before using the normal breaking change process, if we think the impact will be small enough.)

The `@Deprecated` annotation has some intricate rules around inheritance and multiple paths to a declaration or member signature. It can roughly be summarized as: An API feature use is considered deprecated if removing all deprecated declarations/parameters would change the meaning of the use.

In the same way, an API feature use is considered discontinued if removing all declarations/parameters which are deprecated and discontinued at the current language version or below, would change the meaning of the use.

If a declaration is imported more than once, then its use is deprecated only if it's deprecated along all import paths (which it might not be if the deprecation comes from a deprecated `import` or `export` declaration, rather than the declaration itself being deprecated, and the declaration also being imported non-deprecated).
An instance member parameter is deprecated if there is not a non-deprecated occurrence of it in any superinterface of the type (and it would be bad style to deprecate a parameter in a class if it's not deprecated in a superinterface, because then it can't be removed).

In the same way, an API feature use is considered discontinued in a language version if it's removed in that language version along all imports paths/in all superinterface members.

This approach can only disallow access, it doesn't actually remove anything, which means that:
* It's not possible to discontinue a *required* parameter gradually. It's still there, and still required, no matter which language version. The language version must be ignored for required parameters.
* It's not possible to discontinue an optional positional parameter unless all later positional parameters are discontinued too. You can't not pass a value for it and still pass an argument for a later parameter.
* It's not possible to introduce a new, different member or parameter with the same name as a discontinued member. It's still there and blocking the namespace.
* We can't block `dynamic` invocations from using a deprecated member or parameter.

These are really the same rules as for any deprecation, the feature is still there, we just go from "you're not supposed to use it" to "you're not allowed to use it".

This can be made a language feature, and block access in all compilers.

Alternatively, it can be made an Analyzer-only feature, one where violations default to being an *error*. That would be consistent with all other `@Deprecated` features being Analyzer-only. It would mean that if you skip the analyzer, you can keep using deprecated members after their expiry date.

@natebosch

Ob-feature-request: We can't actually make it `Deprecated("message", discontinued: "2.11")` because the positional parameter is already optional. (https://github.com/dart-lang/language/issues/1076)

Contributor guide

Open the contributing guide

Research direction

The issue names no repository files or tests. Start by reading the existing @Deprecated rules summarized here and the linked API-shape discussion in issue #1076, then trace how the compiler and analyzer handle deprecated declarations; done means an agreed design covering inheritance, imports, parameters, dynamic calls, and the compiler-versus-analyzer choice.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.