Deprecated Stuff
Motivation
Prior-art
Proposal
The proposal is to introduce either a new deprecated;
global to the language
or a new 'deprecated';
pragma directive such that deprecated code can be easily
identified in a standard way by tooling and the VM implementations.
For example:
function deprecatedFunction() {
deprecated;
// do stuff
}
// or
function deprecatedFunction() {
'deprecated';
// do stuff
}
Tooling support:
- VM implementations supporting the
deprecated
mechanism can provide an option to throw whendeprecated
code is encountered (similar in idea tonode --throw-deprecation
) or provide an embedder API to receive a callback whendeprecated
code is compiled, allowing embedders like Node.js the ability to custom handle it. (similar in idea to Node.js'process.on('warning', (warning) => { /* ... */ })
API) - VM implementations can automatically de-optimize code in any
deprecated
scope with an option to ignore thedeprecated
statement if necessary. The point is, there should be a penalty-by-default to usedeprecated
code that users would need to explicitly opt-out from. (similar in idea to:node --no-deprecation
) - Debuggers, IDEs, linters, and related tooling can provide visible warnings about
use of
deprecated
code.
Pragma option: 'deprecated';
Alternatively, deprecated;
can be a pragma type instruction:
function deprecatedFunction() {
'deprecated';
// do stuff
}
To support the case where a "deprecation message" should accompany the code, we can use something like:
function deprecatedFunction() {
'deprecated; This is deprecated, use something else';
// do stuff
}
The pragma directive option has the fewest number of edge cases, is the easiest to polyfill, and avoids potential conflicts that could break user code.
Also worth noting is the fact that the pragma directive approach does not
actually require any action on the part of the TC-39 committee. The VM's and
tooling providers can choose to support a 'deprecated';
directive by convention.
Polyfills and Dealing with Ambiguity
To make this easily polyfillable and to avoid breaking existing code, deprecated
here can actually be a global with a very specific value as opposed to a language
keyword. For instance, a new Symbol.deprecated
well-known standard symbol can
be defined with global.deprecated = Symbol.deprecated
by default. Then, when
the VM or tooling encounters deprecated;
in the code, and deprecated
is equal
to Symbol.deprecated
, it is handled as described.
On older VMs that do not understand the special meaning of deprecated;
when it has
been polyfilled, the statement would be a non-op.
VMs should have little difficulty in determining whether or not to de-opt at runtime but tooling can see some ambiguity here.. for instance:
function foo(deprecated) {
deprecated; // Only triggers deprecated semantics in the VM if deprecated === Symbol.deprecated
// Causes ambiguity in tooling because it cannot determine reliably if deprecated === Symbol.deprecated
}
Note: the VM behavior expected here would be an automatic de-opt of the code by default, and possibly a throw if a given flag was enabled.
In this case, the best thing the tooling could do is show a warning about the ambiguous use and provide a mechanism (such as a lint ignore rule) to opt-out of the checking for that particular case.
Another ambiguous case would be what happens if a user assigns Symbol.deprecated
to a different variable. Would it have the same effect? The answer is no. The
explicit requirement is an expression that is exactly deprecated;
(with or without
the ;
but that's a different argument entirely).
function foo() {
const m = deprecated; // does not trigger the 'deprecated' semantics
m; // does not trigger the 'deprecated' semantics
deprecated; // does trigger the 'deprecated' semantics
deprecated // does trigger the 'deprecated' semantics
}
function foo() {
deprecated = 'something else';
deprecated; // does not trigger the 'deprecated' semantics
}
Note: Using the pragma directive option avoids these issues entirely.
We could go with an approach like import { deprecated } from 'something'
to pull in the intrinsic rather
than making it global.
Specifically,
import { deprecated as dep } from 'something'
function foo() {
dep;
}
This would be fine from a VM behavior perspective but makes things more difficult
from a tooling perspective specifically because the tooling would have to perform
additional more complex analysis to know what dep;
actualy means.
Tools like linters may consistently struggle with reliably detecting use of deprecated code even using a mechanism such as this. For instance,
class Foo {
bar() {
deprecated
}
}
require('some-module-that-monkey-patches-Foo')
var foo = new Foo()
foo.bar()
In this case, tools would have to be able to reliably determine if bar()
has
been monkeypatched in order to give any kind of reasonable warning. This is why
relying strictly on tools like linting is not sufficient. There's no silver bullet
around resolving this particular ambiguity.
Alternative: Using Decorators
The Decorators proposal offers a potential alternative approach to this...
@deprecated
function deprecatedFunction() {}
This approach, however, carries with it a number of important limitations:
The syntax is not backwards supportable or easily polyfillable. Specifically, the code would need to be transpiled to remove the decorator in order to run on older versions of Node.js and older browsers while the new global and pragma approach could be easily polyfilled.
Within Node.js, we often have need to deprecated within a function, for instance, in some cases only certain combinations of arguments to a function are deprecated. For the decorator method to work, then, we would need to be able to decorate individual blocks, e.g.,
function foo(...args) {
if (typeof args[0] === 'string') {
// this is ok
} else
@deprecated {
// this is deprecated
}
}
Alternative: deprecated()
Intrinsic
In Node.js, we do not only mark code as being deprecated, we also associate a static identifier and message with the deprecation to provide helpful additional information to the user. While this has proven to have mixed results, we could follow the same basic pattern here while preserving the semantics discussed above.
function deprecatedFunction() {
deprecated('Use something else');
// so some stuff
}
The default VM behavior would be the same: automatic de-opt when encountered with an option of throwing if a given flag is used. The message given to the function would provide the error message text.
This approach is not mutually exclusive with the proposed approach in that rather than defining
global.deprecated === Symbol.deprecated()
, we just make global.deprecated === [Intrinsic function deprecated()]
,
with the same fundamental semantics.
Where this could run in to issues, however, is in the ambiguous case described earlier where deprecated
is
assigned some other value by user code:
function foo(deprecated) {
deprecated('Use something else') // fails if deprecated happens to not be a function
}