Add deprecated / obsolete variable support

By calling the custom KATI_deprecated_var / KATI_obsolete_var functions,
variables may be marked as deprecated or obsolete.

When accessed or assigned, deprecated variables will print a warning.

When accessed or assigned, obsolete variables will print an error and
stop.

Variables do not need to be set before calling the functions, and will
persist the deprecation warning through a reassignment. This way we can
easily mark variables that are sometimes passed via the environment as
deprecated.

Change-Id: Id04c974c446f471a18cc173f817760f4a02b9239
diff --git a/eval.cc b/eval.cc
index 7a08be7..ecfcd50 100644
--- a/eval.cc
+++ b/eval.cc
@@ -55,7 +55,9 @@
        is_override ? VarOrigin::OVERRIDE : VarOrigin::FILE));
 
   Var* rhs = NULL;
+  Var* prev = LookupVarInCurrentScope(lhs);
   bool needs_assign = true;
+
   switch (op) {
     case AssignOp::COLON_EQ: {
       SimpleVar* sv = new SimpleVar(origin);
@@ -67,7 +69,6 @@
       rhs = new RecursiveVar(rhs_v, origin, orig_rhs);
       break;
     case AssignOp::PLUS_EQ: {
-      Var* prev = LookupVarInCurrentScope(lhs);
       if (!prev->IsDefined()) {
         rhs = new RecursiveVar(rhs_v, origin, orig_rhs);
       } else if (prev->ReadOnly()) {
@@ -80,7 +81,6 @@
       break;
     }
     case AssignOp::QUESTION_EQ: {
-      Var* prev = LookupVarInCurrentScope(lhs);
       if (!prev->IsDefined()) {
         rhs = new RecursiveVar(rhs_v, origin, orig_rhs);
       } else {
@@ -91,6 +91,13 @@
     }
   }
 
+  prev->Used(this, lhs);
+  if (prev->Deprecated()) {
+    if (needs_assign) {
+      rhs->SetDeprecated(prev->DeprecatedMessage());
+    }
+  }
+
   LOG("Assign: %s=%s", lhs.c_str(), rhs->DebugString().c_str());
   if (needs_assign) {
     return rhs;
diff --git a/expr.cc b/expr.cc
index 30a25b5..641bf5a 100644
--- a/expr.cc
+++ b/expr.cc
@@ -134,6 +134,7 @@
 
   virtual void Eval(Evaluator* ev, string* s) const override {
     Var* v = ev->LookupVar(name_);
+    v->Used(ev, name_);
     v->Eval(ev, s);
   }
 
@@ -158,7 +159,9 @@
     ev->IncrementEvalDepth();
     const string&& name = name_->Eval(ev);
     ev->DecrementEvalDepth();
-    Var* v = ev->LookupVar(Intern(name));
+    Symbol sym = Intern(name);
+    Var* v = ev->LookupVar(sym);
+    v->Used(ev, sym);
     v->Eval(ev, s);
   }
 
@@ -184,10 +187,12 @@
   virtual void Eval(Evaluator* ev, string* s) const override {
     ev->IncrementEvalDepth();
     const string&& name = name_->Eval(ev);
-    Var* v = ev->LookupVar(Intern(name));
+    Symbol sym = Intern(name);
+    Var* v = ev->LookupVar(sym);
     const string&& pat_str = pat_->Eval(ev);
     const string&& subst = subst_->Eval(ev);
     ev->DecrementEvalDepth();
+    v->Used(ev, sym);
     const string&& value = v->Eval(ev);
     WordWriter ww(s);
     Pattern pat(pat_str);
diff --git a/func.cc b/func.cc
index 7d50219..cc55f70 100644
--- a/func.cc
+++ b/func.cc
@@ -809,6 +809,66 @@
   }
 }
 
+void DeprecatedVarFunc(const vector<Value*>& args, Evaluator* ev, string*) {
+  string vars_str = args[0]->Eval(ev);
+  string msg;
+
+  if (args.size() == 2) {
+    msg = ". " + args[1]->Eval(ev);
+  }
+
+  if (ev->avoid_io()) {
+    ev->Error("*** $(KATI_deprecated_var ...) is not supported in rules.");
+  }
+
+  for (StringPiece var : WordScanner(vars_str)) {
+    Symbol sym = Intern(var);
+    Var* v = ev->LookupVar(sym);
+    if (!v->IsDefined()) {
+      v = new SimpleVar(VarOrigin::FILE);
+      sym.SetGlobalVar(v, false, nullptr);
+    }
+
+    if (v->Deprecated()) {
+      ev->Error(StringPrintf("*** Cannot call KATI_deprecated_var on already deprecated variable: %s.", sym.c_str()));
+    } else if (v->Obsolete()) {
+      ev->Error(StringPrintf("*** Cannot call KATI_deprecated_var on already obsolete variable: %s.", sym.c_str()));
+    }
+
+    v->SetDeprecated(msg);
+  }
+}
+
+void ObsoleteVarFunc(const vector<Value*>&args, Evaluator* ev, string*) {
+  string vars_str = args[0]->Eval(ev);
+  string msg;
+
+  if (args.size() == 2) {
+    msg = ". " + args[1]->Eval(ev);
+  }
+
+  if (ev->avoid_io()) {
+    ev->Error("*** $(KATI_obsolete_var ...) is not supported in rules.");
+  }
+
+  for (StringPiece var : WordScanner(vars_str)) {
+    Symbol sym = Intern(var);
+    Var* v = ev->LookupVar(sym);
+    if (!v->IsDefined()) {
+      v = new SimpleVar(VarOrigin::FILE);
+      sym.SetGlobalVar(v, false, nullptr);
+    }
+
+    if (v->Deprecated()) {
+      ev->Error(StringPrintf("*** Cannot call KATI_obsolete_var on already deprecated variable: %s.", sym.c_str()));
+    } else if (v->Obsolete()) {
+      ev->Error(StringPrintf("*** Cannot call KATI_obsolete_var on already obsolete variable: %s.", sym.c_str()));
+    }
+
+    v->SetObsolete(msg);
+  }
+}
+
 FuncInfo g_func_infos[] = {
   { "patsubst", &PatsubstFunc, 3, 3, false, false },
   { "strip", &StripFunc, 1, 1, false, false },
@@ -852,6 +912,10 @@
   { "error", &ErrorFunc, 1, 1, false, false },
 
   { "file", &FileFunc, 2, 1, false, false },
+
+  /* Kati custom extension functions */
+  { "KATI_deprecated_var", &DeprecatedVarFunc, 2, 1, false, false },
+  { "KATI_obsolete_var", &ObsoleteVarFunc, 2, 1, false, false },
 };
 
 unordered_map<StringPiece, FuncInfo*>* g_func_info_map;
diff --git a/testcase/deprecated_var.mk b/testcase/deprecated_var.mk
new file mode 100644
index 0000000..2cacbda
--- /dev/null
+++ b/testcase/deprecated_var.mk
@@ -0,0 +1,70 @@
+# TODO(go): not implemented
+
+
+A := test
+$(KATI_deprecated_var A B C D)
+
+# Writing to an undefined deprecated variable
+B := test
+ifndef KATI
+$(info Makefile:8: B has been deprecated.)
+endif
+
+# Reading from deprecated variables (set before/after/never the deprecation func)
+# Writing to an undefined deprecated variable
+D := $(A)$(B)$(C)
+ifndef KATI
+$(info Makefile:15: A has been deprecated.)
+$(info Makefile:15: B has been deprecated.)
+$(info Makefile:15: C has been deprecated.)
+$(info Makefile:15: D has been deprecated.)
+endif
+
+# Writing to a reset deprecated variable
+D += test
+ifndef KATI
+$(info Makefile:24: D has been deprecated.)
+endif
+
+# Using a custom message
+$(KATI_deprecated_var E,Use X instead)
+E = $(C)
+ifndef KATI
+$(info Makefile:31: E has been deprecated. Use X instead.)
+endif
+
+# Expanding a recursive variable with an embedded deprecated variable
+$(E)
+ifndef KATI
+$(info Makefile:37: E has been deprecated. Use X instead.)
+$(info Makefile:37: C has been deprecated.)
+endif
+
+# All of the previous variable references have been basic SymRefs, now check VarRefs
+F = E
+G := $($(F))
+ifndef KATI
+$(info Makefile:45: E has been deprecated. Use X instead.)
+$(info Makefile:45: C has been deprecated.)
+endif
+
+# And check VarSubst
+G := $(C:%.o=%.c)
+ifndef KATI
+$(info Makefile:52: C has been deprecated.)
+endif
+
+# Deprecated variable used in a rule-specific variable
+test: A := $(E)
+ifndef KATI
+$(info Makefile:58: E has been deprecated. Use X instead.)
+$(info Makefile:58: C has been deprecated.)
+# A hides the global A variable, so is not considered deprecated.
+endif
+
+# Deprecated variable used in a rule
+test:
+	echo $(C)Done
+ifndef KATI
+$(info Makefile:67: C has been deprecated.)
+endif
diff --git a/testcase/err_deprecated_var_already_deprecated.mk b/testcase/err_deprecated_var_already_deprecated.mk
new file mode 100644
index 0000000..3f110cf
--- /dev/null
+++ b/testcase/err_deprecated_var_already_deprecated.mk
@@ -0,0 +1,4 @@
+# TODO(go): not implemented
+
+$(KATI_deprecated_var A)
+$(KATI_deprecated_var A)$(or $(KATI),$(error Cannot call KATI_deprecated_var on already deprecated variable: A))
diff --git a/testcase/err_deprecated_var_already_obsolete.mk b/testcase/err_deprecated_var_already_obsolete.mk
new file mode 100644
index 0000000..3f60202
--- /dev/null
+++ b/testcase/err_deprecated_var_already_obsolete.mk
@@ -0,0 +1,4 @@
+# TODO(go): not implemented
+
+$(KATI_obsolete_var A)
+$(KATI_deprecated_var A)$(or $(KATI),$(error Cannot call KATI_deprecated_var on already obsolete variable: A))
diff --git a/testcase/err_obsolete_var.mk b/testcase/err_obsolete_var.mk
new file mode 100644
index 0000000..fe032bd
--- /dev/null
+++ b/testcase/err_obsolete_var.mk
@@ -0,0 +1,6 @@
+# TODO(go): not implemented
+#
+# We go into a lot more cases in deprecated_var.mk, and hope that if deprecated works, obsolete does too.
+
+$(KATI_obsolete_var A)
+$(A) $(or $(KATI),$(error A is obsolete))
diff --git a/testcase/err_obsolete_var_already_deprecated.mk b/testcase/err_obsolete_var_already_deprecated.mk
new file mode 100644
index 0000000..42cfbfb
--- /dev/null
+++ b/testcase/err_obsolete_var_already_deprecated.mk
@@ -0,0 +1,4 @@
+# TODO(go): not implemented
+
+$(KATI_deprecated_var A)
+$(KATI_obsolete_var A)$(or $(KATI),$(error Cannot call KATI_obsolete_var on already deprecated variable: A))
diff --git a/testcase/err_obsolete_var_already_obsolete.mk b/testcase/err_obsolete_var_already_obsolete.mk
new file mode 100644
index 0000000..e6f9d76
--- /dev/null
+++ b/testcase/err_obsolete_var_already_obsolete.mk
@@ -0,0 +1,4 @@
+# TODO(go): not implemented
+
+$(KATI_obsolete_var A)
+$(KATI_obsolete_var A)$(or $(KATI),$(error Cannot call KATI_obsolete_var on already obsolete variable: A))
diff --git a/testcase/err_obsolete_var_assign.mk b/testcase/err_obsolete_var_assign.mk
new file mode 100644
index 0000000..a6b7397
--- /dev/null
+++ b/testcase/err_obsolete_var_assign.mk
@@ -0,0 +1,4 @@
+# TODO(go): not implemented
+
+$(KATI_obsolete_var A)
+A := $(or $(KATI),$(error A is obsolete))
diff --git a/testcase/err_obsolete_var_msg.mk b/testcase/err_obsolete_var_msg.mk
new file mode 100644
index 0000000..8162b10
--- /dev/null
+++ b/testcase/err_obsolete_var_msg.mk
@@ -0,0 +1,4 @@
+# TODO(go): not implemented
+
+$(KATI_obsolete_var A,Use Y instead)
+$(A) $(or $(KATI),$(error A is obsolete. Use Y instead))
diff --git a/testcase/err_obsolete_var_varref.mk b/testcase/err_obsolete_var_varref.mk
new file mode 100644
index 0000000..a671d56
--- /dev/null
+++ b/testcase/err_obsolete_var_varref.mk
@@ -0,0 +1,5 @@
+# TODO(go): not implemented
+
+$(KATI_obsolete_var A)
+B := A
+$($(B)) $(or $(KATI),$(error A is obsolete))
diff --git a/testcase/err_obsolete_var_varsubst.mk b/testcase/err_obsolete_var_varsubst.mk
new file mode 100644
index 0000000..33c89b5
--- /dev/null
+++ b/testcase/err_obsolete_var_varsubst.mk
@@ -0,0 +1,4 @@
+# TODO(go): not implemented
+
+$(KATI_obsolete_var A)
+$(A:%.o=%.c) $(or $(KATI),$(error A is obsolete))
diff --git a/var.cc b/var.cc
index 8f46ff8..1e6f5bb 100644
--- a/var.cc
+++ b/var.cc
@@ -16,6 +16,7 @@
 
 #include "var.h"
 
+#include "eval.h"
 #include "expr.h"
 #include "log.h"
 
@@ -37,7 +38,7 @@
   return "*** broken origin ***";
 }
 
-Var::Var() : readonly_(false) {
+Var::Var() : readonly_(false), message_(), error_(false) {
 }
 
 Var::~Var() {
diff --git a/var.h b/var.h
index 75653de..15f461e 100644
--- a/var.h
+++ b/var.h
@@ -15,11 +15,14 @@
 #ifndef VAR_H_
 #define VAR_H_
 
+#include <memory>
 #include <string>
 #include <unordered_map>
 #include <unordered_set>
 
+#include "eval.h"
 #include "expr.h"
+#include "log.h"
 #include "stmt.h"
 #include "string_piece.h"
 #include "symtab.h"
@@ -59,11 +62,39 @@
   bool ReadOnly() const { return readonly_; }
   void SetReadOnly() { readonly_ = true; }
 
+  bool Deprecated() const { return message_ && !error_; }
+  void SetDeprecated(StringPiece msg) {
+    message_.reset(new string(msg.as_string()));
+  }
+
+  bool Obsolete() const { return error_; }
+  void SetObsolete(StringPiece msg) {
+    message_.reset(new string(msg.as_string()));
+    error_ = true;
+  }
+
+  const string& DeprecatedMessage() const { return *message_; }
+
+  // This variable was used (either written or read from)
+  void Used(Evaluator* ev, const Symbol& sym) const {
+    if (!message_) {
+      return;
+    }
+
+    if (error_) {
+      ev->Error(StringPrintf("*** %s is obsolete%s.", sym.c_str(), message_->c_str()));
+    } else {
+      WARN_LOC(ev->loc(), "%s has been deprecated%s.", sym.c_str(), message_->c_str());
+    }
+  }
+
  protected:
   Var();
 
  private:
   bool readonly_;
+  unique_ptr<string> message_;
+  bool error_;
 };
 
 class SimpleVar : public Var {