Different levels of `@[Deprecation]` annotations?

The @[Deprecation] annotation is a useful tool to help keep code bases up to date.
But sometimes its use could be a bit more nuanced.

Deprecations are often used when a new method is introduced and intents to replace an existing one (say Time.instant replaces Time.monotonic, see When are folks removing `Time.monotonic` usage from their shards?).
The trouble is that we go from one state directly to another one.

  1. Time.monotonic is fully supported, Time.instant doesn’t even exist.
  2. Time.instant exists and Time.monotonic is deprecated and may trigger lots of deprecation warnings.

It would be nice to have a transitioning period between introducing Time.instant and deprecating Time.monotonic.

  1. Time.monotonic is fully supported, Time.instant doesn’t even exist.
  2. Time.instant gets introduced.
  3. Time.monotonic is deprecated.

We can easily do this be delaying the deprecation for a couple of releases. As long as the old method is not fundamentally broken, there is no urgency.
This allows users to adopt the new method without being annoyed by deprecation messages.

The problem now is that there is no mechanic to indicate that Time.monotonic should be avoided. For example, you might want to prevent introducing new code using that old method. We can add a note to the API docs, but that won’t tell you whether it’s used in your code base.

So I think maybe the @[Deprecation] annotation should support different levels of deprecation (this is used in Elixir, for example. See When are folks removing `Time.monotonic` usage from their shards? - #5 by ysbaddaden).

Soft deprecation indicates that a method should be avoided, but doesn’t issue compiler warnings by default. It should be possible to opt in to that, in order to avoid introducing new calls to soft-deprecated methods.

Hard deprecation indicates that a method should not be used. It issues compiler warnings by default.

While doing that, it might also be helpful to document expected time frame for deprecation.
For example, soft deprecation started in 1.19 and hard deprecation is expected for 1.23.
Potentially, the compiler could even figure out the current state by comparing the indicated versions:

@[Deprecated("Use `Time.instant` instead", soft: "1.19", hard: "1.23")]

WDYT?

1 Like

I like the direction you’re going in (especially the idea of making deprecation less abrupt), but I’m not convinced that adding “soft” and “hard” levels to @[Deprecated] is the right abstraction for Crystal.

Looking at how other languages handle this, there are basically two successful models:

  1. Severity-based (Kotlin/C#): WARNING / ERROR / HIDDEN
  2. Lifecycle-based (Swift/Rust): introduced → deprecated → obsoleted (version-driven)

What tends to scale better long term is the lifecycle model, not multiple subjective “levels”.

If we add soft: and hard: fields, we’re effectively encoding policy (“how loud should this be?”) into the annotation. That mixes two concerns:

  • When something is deprecated
  • How strictly the compiler should treat it

Other ecosystems usually separate those.

A more robust approach for Crystal would be to make deprecation version-aware and let severity naturally escalate over time. For example:

@[Deprecated(
  message: "Use `Time.instant` instead",
  since: "1.19",
  error_in: "1.23",
  remove_in: "2.0"
)]

Compiler behavior would be derived from its own version:

  • Before since: no warning
  • From since until error_in: warning
  • From error_in: error
  • From remove_in: symbol gone

This gives you the “transitioning period” you’re asking for without inventing a new conceptual level like “soft”. The transition is implicit in the timeline.

For the earlier “avoid new usage but don’t warn yet” phase, I wouldn’t put that into @[Deprecated] at all. In most languages, that’s either:

  • A documentation-level signal
  • A linter rule
  • A separate lightweight annotation (e.g. @[Discouraged]) that tooling can opt into

That keeps the compiler simple and avoids warning fatigue, while still giving teams a way to enforce stricter policies in CI.

//Ali

1 Like

Maybe on soft-deprecation the warning could be shown with --release? That way you don’t get it while developing.

The soft vs hard deprecation levels come from Elixir, where it’s merely a stdlib policy. It’s incredibly valuable that deprecations are grouped into a deprecation table :heart:

A problem with formalizing soft/hard as annotation metadata, is that the compiler will have to consider the version of every shard and link sets of files to a shard to decide if it must emit a warning (today the compiler doesn’t even consider shards).

Also, we won’t even be able to use it in stdlib for a while :disappointed_face:

 1 | @[Deprecated("message", soft: "1.19.0", hard: "1.23.0")]
     ^
Error: too many named arguments (given 2, expected maximum 0)

Maybe we don’t need to formalize a particular policy into the annotation, at least until we can use the named args in stdlib?

We could add a DEPRECATED: message to the documentation (improving crystal docs to handle it) and keep a manual list of deprecated features, then after N+ releases check the list and replace the DEPRECATED: messages with the actual @[Deprecated] annotation.

Yeah, I suppose a version-based automatic classification would be hard to achieve.
It would still be useful to have this kind of historic information. Though maybe it should be considered a documentation feature. It could also include more lifecycle information, for example when a method was introduced, or when a parameter was added etc.

I would like “soft” deprecation to be as easily detectable as current “hard” deprecations. It should be possible to run the compiler with a strict configuration that alerts about “soft” deprecations.
Maybe a @[Discouraged] annotation could be a tool for that. But in my mind, this just describes a different level of @[Deprecated] and should not be considered a different concept.

1 Like

Yes, soft deprecation isn’t only discouraged, it’s deprecated, we just don’t yell loudly (yet).
Yes, detecting and reporting soft-deprecations on-demand would be nice.

I don’t know. On one hand I really like dumb stupid deprecation rules, but perhaps more is needed. :person_shrugging:

1 Like

I think that softer warning will not push for migration. The sooner the migration happens the sooner the feedback.

In the Time example, when both methods exists, without the noisy warning i doubt people will transition to the new method. It’s valuable that the transition is possible progressively. I see that maybe is too noisy, but i think that is needed.

Maybe a piece we are missing is a way to ignore some specific warnings. Either for quieter development or because the migration needs to be postponed. So a way to mark a callsite to don’t trigger the warning.

Let’s remember that the compiler doesn’t know about shards and is a good principle.

Are the keyword arguments intended for specifying versions of Crystal only or would a shard also be able to use them?

It should be the shard version. But determining that would be impractical, so I don’t think this aspect is going to work.

Yeah, perhaps another angle to address this space is to improve deprecation tooling.
For example, the default warnings could be less noisy and just report which deprecated features are used, and not print 5 lines for every single use of a deprecated feature.

Reducing the impact strength of deprecation warnings would make them less intimidating.

+1 to this approach, both of them: 1. just showing one warning per run, 2. being able to disable some of them (though that will make them need an identifier)

@straight-shoota We should definitely group warnings: print a deprecation message once with the list of individual calls, maybe limit it to 3 or 5 calls + and X other usages message, or X call sites, for example:\n - file:line\n ....

@bcardiff Soft deprecation only delays the migration (new code shouldn’t use), after N releases it becomes a hard deprecation and the compiler will start warning you (old code shouldn’t use)

If you silence a specific warning the compiler won’t warn you anymore, and you never get the incentive, or reminder, to migrate old code. I don’t think it’s a good idea.