68

In my team we have been cleaning a lot of old stuff in a big monolithic project (whole classes, methods, etc.).

During that cleaning tasks I was wondering if there is a kind of annotation or library fancier than the usual @Deprecated. This @FancyDeprecated should prevent the build of the project from succeeding if you haven't cleaned old unused code after a particular date has passed.

I have been searching in the Internet and didn't find anything that have the capabilities described below:

  • should be an annotation, or something similar, to place in the code you are intended to delete before a particular date
  • before that date the code will compile and everything will work normally
  • after that date the code will not compile and it will give you a message warning you about the problem

I think I am searching for an unicorn... Is there any similar technology for any program language?

As a plan B I am thinking of the possibility of making the magic with some unit tests of the code that is intended to be removed that start to fail at the "deadline". What do you think about this? Any better idea?

closed as off-topic by Thomas Owens Mar 18 '18 at 23:05

This question appears to be off-topic. The users who voted to close gave this specific reason:

  • "Questions asking us to find or recommend tools, libraries, programming languages, resources (including books, blogs, tutorials, and examples), or projects to undertake are off-topic here as they attract opinionated answers that won't have lasting value to others. You may be able to get help in The Whiteboard, our chat room." – Thomas Owens
If this question can be reworded to fit the rules in the help center, please edit the question.

  • 167
    Deprecation is done for a version, not a date. If you want people to stop using deprecated features, release a version without them. That'll get their attention, and they can always roll back if they can't do it yet. Plenty of people are stuck on old versions. Breaking their program is not the way to go. – isanae Mar 8 '18 at 12:28
  • 4
    Plan B sounds solid. With the added advantages that you can put comment in the unit test as to why it's deprecated. Adding the comment to the source code wouldn't help readability in a big monolith – dwana Mar 8 '18 at 13:19
  • 52
    That sounds like a really evil unicorn. You have a bit of software that compiles fine, passes all the tests, and you've shipped it to a customer. Then one day, you check out the software again and it won't build, even on exactly the same development platform. You're now forced to modify code that previously passed formal testing, or do evil hacks like rolling back the computer's clock. – Simon B Mar 8 '18 at 16:42
  • 6
    Make it @Deprecated_RemoveMe_2018_06_01 and then on 2018-06-01 delete the annotation itself. Voila, all your code using the annotation will no longer compile! (because the annotation isn't found) – immibis Mar 8 '18 at 22:28
  • 64
    Years in the future, a newly hired developer will ask: why is the time on the build server set to January 2016? And the guy who's there for 10y will tell him that it's necessary or the build breaks in random places. Sigh. – Wilbert Mar 9 '18 at 7:09

12 Answers 12

62

I don't think this would be a useful feature when it really prohibits compilation. When at 01/06/2018 large parts of the code won't compile which compiled the day before, your team will quickly remove that annotation again, code cleaned up or not.

However, you could add some custom annotation to the code like

@Deprecated_after_2018_07_31

and build a small tool to scan for those annotations. (A simple one liner in grep will do it, if you don't want to utilize reflection). In other languages than Java, a standardized comment suitable for "grepping", or a preprocessor definition can be used.

Then run that tool shortly before or after the particular date, and if it still finds that annotation, remind the team to clean up those code parts urgently.

  • 9
    @Bergi: hey, this is just an example, OP can use versions or date tags, whatever they prefer in their controlling. – Doc Brown Mar 8 '18 at 16:11
  • 4
    I don't see any advantage to doing this over just using the normal deprecation features and looking at the compiler's output on the specific date. It seems like a poor use of developer time to re-implement something that pretty much already exists. Even if you've got a ton of warnings already (which it would be worth your time to clean up), surely you can have the compiler spit all the warnings out to a text file and then grep or search it. – jpmc26 Mar 9 '18 at 13:14
  • 4
    @jpaugh I'm recommending not sinking hours (note, time = money) into building new tools to do tasks that you can already do efficiently with existing tools, whatever those tools may be. – jpmc26 Mar 9 '18 at 15:53
  • 3
    @jpmc26: I see two (small) advantages: not getting tons of deprecation warnings (which would introduce a risk of overlooking more important ones), and a possibility of introducing different deprecation criteria (dates, versions, whatever) for different code sections. Moreover, the OP asked explicitly for coupling deprecation with a date. – Doc Brown Mar 9 '18 at 17:02
  • 7
    I would +1 if you would just use YYYY-MM-DD order for the date in your example, because I've only ever seen the ambiguous ??-??-YYYY order cause pain and misunderstandings. – mtraceur Mar 10 '18 at 1:50
285

This would constitute a feature known as a time bomb. DON'T CREATE TIME BOMBS.

Code, no matter how well you structure and document it, will turn into an ill-understood near-mythical black box if it lives beyond a certain age. The last thing anyone in the future needs is yet another strange failure mode that catches them totally by surprise, at the worst possible time, and without an obvious remedy. There is absolutely no excuse for intentionally producing such a problem.

Look at it this way: if you're organized and aware of your code base enough that you care about obsolescence and follow through on it, then you don't need a mechanism within the code to remind you. If you're not, chances are that you also not up-to-date on other aspects of the code base, and will probably be unable to respond to the alarm timely and correctly. In other words, time bombs serve no good purpose for anyone. Just Say No!

  • 74
    +1 This will bite you. Months later. On a Friday just as you're trying to do an emergency deploy. And it's a long weekend, so all the people who know anything about it are out of the office. Don't do it. – Roger Lipscombe Mar 8 '18 at 14:06
  • 3
    Agree. Obsolescence is an organizational issue, not a coding issue. I could still plug in an Apple ][ and write BASIC, nothing stopping me. But, would I want to? – user251748 Mar 8 '18 at 15:02
  • 19
    The correct solution, going with your "organized and aware" direction, is to raise a bug in your bug tracking system, saying "Remove <such and such>", with a suggested timeframe in the comments. And then in 6 months when bug review comes around to find out what bugs should get fixed in the next release, if that timeframe is imminent, you say "it's time to do this one". It's basic project management. – Lightness Races in Orbit Mar 8 '18 at 15:06
  • 1
    In fact, if your release schedule is sufficiently predictable, just milestone it now. – Lightness Races in Orbit Mar 8 '18 at 15:08
  • 13
    Refer to isanae's comment for an alternative: "If you want people to stop using deprecated features, release a version without them. That'll get their attention, and they can always roll back if they can't do it yet." – Steven M. Vascellaro Mar 8 '18 at 16:03
70

In C# you would use the ObsoleteAttribute in the following manner:

  • In version 1, you ship the feature. A method, class, whatever.
  • In version 2, you ship a better feature intended to replace the original feature. You put an Obsolete attribute on the feature, set it to "warning", and give it a message that says "This feature is deprecated. Use the better feature instead. In version 3 of this library, which will be released on such and such a date, use of this feature will be an error." Now users of the feature can still use it, but have time to update their code to use the new feature.
  • In version 3, you update the attribute to be an error rather than a warning, and update the message to say "This feature is deprecated. Use the better feature instead. In version 4 of this library, which will be released on such and such a date, this feature will throw." Users who failed to heed your previous warning still get a helpful message that tells them how to fix the problem, and they must fix it, because now their code doesn't compile.
  • In version 4, you change the feature so that it throws some fatal exception, and change the message to say that the feature will be removed entirely in the next version.
  • In version 5, you remove the feature entirely, and if users complain, well hey, you gave them three release cycles of fair warning, and they can always just keep on using version 2 if they feel strongly about it.

The idea here is to make a breaking change as painless as possible for the users affected, and to ensure that they can continue to use the feature for at least one version of the library.

  • 2
    I believe this is the correct approach but I would change one thing... that would be swapping "if users complain" to "when users complain" – Liath Mar 9 '18 at 17:43
  • 10
    Removing the body at the same time as changing over to an error seems odd. One of the advantages of the error version of deprecated, is that it allows code built against previous versions to continue to function while preventing newly compiled code from using it. Thus you remain ABI-compatible, while deliberately breaking API compatibility. Of course perfect world you would have semver style versioning, etc, such that you would never run the new code with a previously compiled application, but many projects never reach that ideal. – Kevin Cathcart Mar 9 '18 at 20:18
  • @KevinCathcart: That's a good point; maybe another stage in there is warranted! I'll update the text. – Eric Lippert Mar 9 '18 at 20:25
  • 1
    Actually, if you're doing SemVer, you can go straight from deprecated in version X.Y.0 (any deprecation must be a minor release) to completely removed in X+1.0.0. I'd say it's nice to hold off a bit further, (to X+2.0.0?), but this five-step process is probably gilding the lily. The next step after "warning" should be "fatal error" (because the deprecated feature is replaced with the error-generating code). Since libfoo.so.2 and libfoo.so.3 can coexist with one another just fine, your downstream can keep using the old library until they get switched over. – Monty Harder Mar 12 '18 at 20:55
  • @MontyHarder: Sure. The exact sequence of events can be determined by the needs of the developer and their user community. The larger point though is that there should be a thought-through policy for dealing with these sorts of versioning issues that is clearly communicated to stakeholders. – Eric Lippert Mar 12 '18 at 21:05
24

You've misunderstood what "deprecated" means. Deprecated means:

be usable but regarded as obsolete and best avoided, typically because it has been superseded.

Oxford Dictionaries

By definition, a deprecated feature will still compile.

You are looking to remove the feature on a specific date. That's fine. The way you do that is you remove it on that date.

Until then, mark as deprecated, obsolete, or whatever your programming language calls it. In the message, include the date it will be removed and the thing that replaces it. This will generate warnings, indicating that other developers should avoid new usage and should replace old usage wherever possible. Those developers will either comply or ignore it, and someone will have to deal with the consequences of that when it's removed. (Depending on the situation, it might be you or it might be the developers using it.)

  • Interested in what the downvoter disagrees with. – jpmc26 Mar 8 '18 at 22:52
  • 2
    +1. If you use JIRA for agile development, create a task and put it in a future sprint; adapt for whatever other project management tools and methodology you follow. This is best solved with non-technical methods. – Matthew Read Mar 8 '18 at 22:53
  • 1
    And also make sure the Class / Lib remains in the documentation as depreciated and/or removed in version X.x. – Phil M Mar 12 '18 at 21:31
12

Don't forget that you need to retain the ability to build and debug older versions of the code in order to support versions of the software that have already been released. Sabotaging a build after a certain date means that you also risk preventing yourself from doing legitimate maintenance and support work in the future.

Also, it seems like a trivial workaround to set my machine's clock back a year or two before compiling.

Remember, "deprecated" is a warning that something will be going away in the future. When you want to forcefully prevent people from using that API, just remove the associated code. There's no point in leaving code in the code base if some mechanism makes it unusable. Removing the code gives you the compile-time checks you're looking for, and doesn't have a trivial workaround.

Edit: I see you refer to "old unused code" in your question. If the code really is unused, there's no point in deprecating it. Just delete it.

  • The best answer by far, maybe stress that deleting the code is the best way to solve the problem sooner in your answer. It can be easy to scroll past wall of text answers – Clint Mar 12 '18 at 15:29
6

I have never seen such a feature before - an annotation that starts taking effect after a particular date.

The @Deprecated can be sufficient, however. Catch warnings in CI, and make it refuse to accept the build if any are present. This shifts the responsibility from the compiler to your build pipeline, but has the advantage that you can (semi)easily alter the build pipeline by adding additional steps.

Note that this answer does not fully solve your problem (e.g. local builds on developers' machines would still succeed, although with warnings) and assumes that you have a CI pipeline set up and running.

4

You are looking for calendars or todo lists.

Another alternative is to use custom compiler warnings or compiler messages, iff you manage to have few if any warnings in your codebase. If you have too many warnings, you'll need to spend additional effort (about 15 minutes?) and have to pick up the compiler warning in the build report which your continuous integration delivers on each build.

Reminders that code needs to be fixed are good and necessary. Sometimes these reminders do have strict real world deadlines, so putting them on a timer may be necessary as well.

The goal is to continuously remind people that the issue exists and needs to be fixed withing a given timeframe - a feature that simply breaks the build at a specific time not only doesn't do that, but that feature is itself an issue that needs to be fixed withing a given timeframe.

  • 1
    Agree. But if an issue exists and needs to be fixed within a given timeframe, then it needs to become someone's top priority at the right time. I remember when in a meeting my Manager gave me a "top priority", then later said, "This is your number one priority" (something different). My Supervisor said, "He can't have two number one priorities!" The Manager was startled. I guess he thought that... I could. Planning for something to be fixed "at the right time" is planning to run out of time. – user251748 Mar 8 '18 at 15:09
3

One way to think about this is what you mean by time/date? Computers don't know what these concepts are: they have to be programmed in somehow. It's quite common to represent times in the UNIX format of "seconds since the epoch", and it's common to feed a particular value into a program via OS calls. However, no matter how common this usage is, it's important to keep in mind that it's not the "actual" time: it is just a logical representation.

As others have pointed out, if you made a "deadline" using this mechanism, it's trivial to feed in a different time and break that "deadline". The same goes for more elaborate mechanisms like asking an NTP server (even over a "secure" connection, since we can substitute our own certificates, certificate authorities or even patch the crypto libraries). At first it might appear that such individuals are at fault for working around your mechanism, but it may be the case that it's done automatically and for good reasons. For example, it's a good idea to have reproducible builds, and tools to help this might automatically reset/intercept such non-deterministic system calls. libfaketime does exactly that, Nix sets all file's timestamps to 1970-01-01 00:00:01, Qemu's record/replay feature fakes all hardware interaction, etc.

This is similar to Goodhart's law: if you make a program's behaviour depend on the logical time, then the logical time ceases to be a good measure of the "actual" time. In other words, people generally won't mess with the system clock, but they will if you give them a reason to.

There are other logical representations of time: one of them is the software's version (either your app or some dependency). This is a more desirable representation for a "deadline" than e.g. UNIX time, since it's more specific to the thing you care about (changing feature sets/APIs) and hence less likely to trample on orthogonal concerns (e.g. fiddling with the UNIX time to work around your deadline could end up breaking log files, cron jobs, caches, etc.).

As others have said, if you control the library and want to "push" this change, you can push a new version which deprecates the features (causing warnings, to help consumers find and update their usage), then another new version which removes the features entirely. You could publish these immediately after each other if you like, since (again) versions are merely a logical representation of time, they need not be related to the "actual" time. Semantic versioning may help here.

The alternative model is to "pull" the change. This is like your "plan B": add a test to the consuming application, which checks that the version of this dependency is at least the new value. As usual, red/green/refactor to propagate this change through the codebase. This may be more appropriate if the functionality isn't "bad" or "wrong", but just "a bad fit for this use-case".

An important question with the "pull" approach is whether or not the dependency version counts as a "unit" (of functionality), and hence deserves testing; or whether it's just a "private" implementation detail, which should only be exercised as part of actual unit (of functionality) tests. I'd say: if the distinction between the dependency's versions really does count as a feature of your application, then do the test (for example, checking that the Python version is >= 3.x). If not, then don't add the test (since it will be brittle, uninformative and overly restrictive); if you control the library then go down the "push" route. If you don't control the library then just use whatever version is provided: if your tests pass then it's not worth restricting yourself; if they don't pass then that's your "deadline" right there!

There is another approach, if you want to discourage certain uses of a dependency's features (e.g. calling certain functions which don't play well with the rest of your code), especially if you don't control the dependency: have your coding standards forbid/discourage the use of these features, and add checks for them to your linter.

Each of these will be applicable in different circumstances.

1

You manage this at package or library level. You control a package and control its visibility. You are free to retract visibility. I've seen this internally at large companies and it only makes sense in cultures that respect ownership of packages even if the packages are open source or free to use.

This is always messy because the client teams simply don't want to change anything, so you often need some rounds of whitelist-only as you work with the specific clients to agree on a deadline to migrate, possibly offering them support.

  • I like the support part. All changes happen smoother, more pleasantly and probably with fewer difficulties if the people promoting them offer support. That is partly a psychological effect: Good support is cooperative; it takes all involved serious. By contrast, changes imposed from above without involving the concerned make them feel ignored and uncooperative. (Although the friendliness must be paired with a relentless drive to get the changes through.) – Peter A. Schneider Mar 9 '18 at 13:34
1

One requirement is to introduce a notion of time into the build. In C, C++, or other languages/build systems which use a C-like preprocessor1, one could introduce a time stamp through defines for the preprocessor at build time: CPPFLAGS=-DTIMESTAMP()=$(date '+%s'). This would likely happen in a makefile.

In the code one would compare that token and cause an error if time is up. Note that using a function macro catches the case that somebody didn't define TIMESTAMP.

#if TIMESTAMP() == 0 || TIMESTAMP() > 1520616626
#   error "The time for this feature has run out, sorry"
#endif

Alternatively, one could simply "define out" the code in question when the time has come. That would allow the program to compile, provided nobody uses it. Say, we have a header defining an api, "api.h", and we don't allow calling old() after a certain time:

//...
void new1();
void new2();
#if TIMESTAMP() < 1520616626
   void old();
#endif
//...

A similar construct would probably eliminate old()'s function body from some source file.

Of course this is not fool proof; one can simply define an old TIMESTAMP in case of the Friday night emergency build mentioned elsewhere. But that is, I think, rather advantageous.

This obviously works only when the library is re-compiled — after that the obsolete code simply does not exist any more in the library. It would not prevent client code from linking to obsolete binaries though.


1 C# only supports the simple definition of preprocessor symbols, no numerical values, which makes this strategy not viable.

  • It is also worth noting that this preprocessor define will force all code that uses TIMESTAMP to be recompiled on every build. It will also incapacitate tools like ccache. This means that the typical compile time for incremental builds will increase significantly depending on how much of the code base is affected by features deprecated in this manner. – mindriot Mar 10 '18 at 23:10
  • @mindriot That's an interesting aspect. I suppose that's true with every method introducing a notion of time into the code though -- the OP said explicitly "after a particular date has passed". One could handle the time aspect in the build system though and leave the code alone, true. But the OP asked explicitly for "something put into the code". My solution has the advantage of being independent of any particular build method. It can be cheated, but you have to cater to it one way or another. – Peter A. Schneider Mar 11 '18 at 6:58
  • You're absolutely right. Your solution here is essentially the only one that provides a practical solution to the OP's actual question. Nevertheless I found it important to point out the downside. It'll be up to the OP to weigh the pros and cons. I think a healthy middle ground could even be achieved by, say, dividing the TIMESTAMP value by, say, 86400, to get a daily granularity and thus fewer recompiles. – mindriot Mar 11 '18 at 13:56
0

In Visual Studio, you can set up a pre-build script that throws an error after a certain date. This will prevent compilation. Here's a script that throws an error on or after March 12, 2018 (taken from here):

@ECHO OFF

SET CutOffDate=2018-03-12

REM These indexes assume %DATE% is in format:
REM   Abr MM/DD/YYYY - ex. Sun 01/25/2015
SET TodayYear=%DATE:~10,4%
SET TodayMonth=%DATE:~4,2%
SET TodayDay=%DATE:~7,2%

REM Construct today's date to be in the same format as the CutOffDate.
REM Since the format is a comparable string, it will evaluate date orders.
IF %TodayYear%-%TodayMonth%-%TodayDay% GTR %CutOffDate% (
    ECHO Today is after the cut-off date.
    REM throw an error to prevent compilation
    EXIT /B 2
) ELSE (
    ECHO Today is on or before the cut-off date.
)

Make sure to read the other answers on this page before using this script.

-1

I understand the objective of what you're trying to do. But as others have mentioned, the build system/compiler is probably not the right place to enforce this. I'd suggest the more natural layer to enforce this policy is either the SCM or environment variables.

If you do the latter, basically add a feature flag that marks a pre-deprecation run. Every time you construct the deprecated class or call a deprecated method, check the feature flag. Just define a single static function assertPreDeprecated() and add this to every deprecated code path. If it's set, ignore assert calls. If it's not throw in an exception. Once the date rolls past, unset the feature flag in the runtime environment. Any lingering deprecated calls to the code will show up in runtime logs.

For an SCM based solution, I'll assume you're using git and git-flow. (If not, the logic is easily adaptable to other VCS's). Create a new branch postDeprecated. In that branch delete all the deprecated code, and begin working on removing any references until it compiles. Any normal changes continue to make to the master branch. Keep merging any non-deprecated related code changes in master back into postDeprecated to minimize integration challenges.

After the deprecation date ends, create a new preDeprecated branch from master. Then merge postDeprecated back into master. Assuming your release goes off the master branch, you should now be using the post-deprecated branch after the date. If there's an emergency, or you can't deliver results in time, you can always rollback to preDeprecated, and make any needed changes on that branch.

  • 7
    That approach sounds like a logistical nightmare. If you start maintaining near-duplicate parallel branches you will end up spending all of your time doing so. Total waste. – Lightness Races in Orbit Mar 8 '18 at 15:08
  • 6
    I cannot agree that maintaining parallel branches, using one simply to remove deprecated functions many versions before you actually want to remove them from the product, is in any way "the industry standard". What is the purpose of that? Yes, a VCS can automatically perform some merges, but a merge should be performed with a developer's eye in order to resolve logical conflicts (and what happens when you get a conflict that can't even be lexically resolved?). Having a build system arbitrarily merge every day for this is just ... pointless. Remove the feature when it's time to remove it. – Lightness Races in Orbit Mar 8 '18 at 18:41
  • 3
    Because there is a good reason to maintain release branches (backporting important patches). There is no good reason to maintain a "something I might do in the future" branch. It's overhead, for no benefit. Simple as that. – Lightness Races in Orbit Mar 8 '18 at 18:51
  • 4
    Where did I get it? It's literally the definition of deprecation. You deprecate something that you might remove in the future. Thus there is no disingenuity, no moving of the goal posts, and certainly no making things up just "to win the argument". This is not "a textbook use case of SCM branching" in any organisation that I want to work for! I guess we will have to agree to disagree. Have a good evening! – Lightness Races in Orbit Mar 8 '18 at 19:11
  • 2
    @lightness - There are many many reasons why officially deprecating code isn't just a "thing we might do whenever we get around to it". Maybe the deprecated code uses a library, where official support is being dropped. Maybe the deprecated code is IP who's license is expiring after a fixed date. Maybe after the given date, there's new regulations, and the code isn't compliant. Your solution is all well and good if you live in an ivory tower. But real-world orgs deal with these type of situations all the time. Software needs to satisfy the needs of the business, not vice versa. – user79126 Mar 9 '18 at 10:15

Not the answer you're looking for? Browse other questions tagged or ask your own question.