static-check-gc-attributes
author Benjamin Smedberg <benjamin@smedbergs.us>
Sat, 26 Jul 2008 22:49:39 -0400
changeset 167 a4da40849f5436e629c5732f4368c6c48189637f
parent 163 1124940b811904ccbf0aa91533a7184843788d7b
permissions -rw-r--r--
State as of now

Add static checking of gc types and correct inheritance of GCFinalizable. Storage-class analysis is getting more precise. See http://wiki.mozilla.org/XPCOMGC/Static_Checker for some readable descriptions.

This doesn't yet re-implement finalizer correctness, thought it will now that we have a reasonable pretense of storage classes. Come to think of it, I should be more precise right now about non-managed classes with destructors... these need to be marked NS_GCOK
* * *
* * *

diff --git a/config/config.mk b/config/config.mk
--- a/config/config.mk
+++ b/config/config.mk
@@ -512,6 +512,7 @@ TREEHYDRA_MODULES = \
 TREEHYDRA_MODULES = \
   $(topsrcdir)/xpcom/analysis/outparams.js \
   $(topsrcdir)/xpcom/analysis/stack.js \
+  $(topsrcdir)/xpcom/analysis/gc.js \
   $(NULL)
 
 DEHYDRA_ARGS = \
diff --git a/xpcom/analysis/gc.js b/xpcom/analysis/gc.js
new file mode 100644
--- /dev/null
+++ b/xpcom/analysis/gc.js
@@ -0,0 +1,131 @@
+include("unstable/lazy_types.js");
+include("gcc_util.js");
+
+function process_type(c)
+{
+  if (!c.isIncomplete && (c.kind == 'class' || c.kind == 'struct')) {
+    if (inheritsFromGCFinalizable(c) &&
+	!leftmostFinalizable(c))
+      error("MMgc::GCFinalizable is not the left-most base of class " + c.name, c.loc);
+
+    if (isXPCOMGCFinalizedObject(c) &&
+	!leftmostFinalizable(c) &&
+       c.name != "XPCOMGCFinalizedObject")
+      error("MMgc::GCFinalizable is not the left-most base of class " + c.name + " which inherits from XPCOMGCFinalizedObject", c.loc);
+  }
+}
+
+/**
+ * is the left-most base of this class (exluding XPCOMGCFinalizedObject)
+ * MMgc::GCFinalizable?
+ */
+function leftmostFinalizable(c)
+{
+  function calculate()
+  {
+    if (c.name == 'MMgc::GCFinalizable')
+      return true;
+
+    if (!c.bases)
+      return false;
+
+    if (c.bases.length > 0 &&
+	leftmostFinalizable(c.bases[0]))
+      return true;
+
+    if (c.bases.length > 1 &&
+	(c.bases[0].name == 'XPCOMGCFinalizedObject' ||
+	 c.bases[0].name == 'XPCOMGCObject') &&
+	leftmostFinalizable(c.bases[1]))
+      return true;
+
+    return false;
+  }
+
+  if (!c.hasOwnProperty('leftmostFinalizable'))
+    c.leftmostFinalizable = calculate();
+
+  return c.leftmostFinalizable;
+}
+
+/**
+ * is MMGc::GCFinalizable anywhere in this class lineage?
+ */
+function inheritsFromGCFinalizable(c)
+{
+  function calculate()
+  {
+    if (c.name == 'MMgc::GCFinalizable')
+      return true;
+
+    for each (let base in c.bases) {
+      if (inheritsFromGCFinalizable(base))
+	return true;
+    }
+
+    return false;
+  }
+
+  if (!c.hasOwnProperty('inheritsFromGCFinalizable'))
+    c.inheritsFromGCFinalizable = calculate();
+
+  return c.inheritsFromGCFinalizable;
+}
+
+/**
+ * is XPCOMGCFinalizedObject anywhere in this class lineage?
+ */
+function isXPCOMGCFinalizedObject(c)
+{
+  function calculate()
+  {
+    if (c.name == 'XPCOMGCFinalizedObject')
+      return true;
+
+    if (!c.bases)
+      return false;
+
+    for each (let base in c.bases)
+      if (isXPCOMGCFinalizedObject(base))
+	return true;
+
+    return false;
+  }
+
+  if (!c.hasOwnProperty('isXPCOMGCFinalizedObject'))
+    c.isXPCOMGCFinalizedObject = calculate();
+
+  return c.isXPCOMGCFinalizedObject;
+}
+
+function process_function(f, stmts)
+{
+  var stmt;
+  function getLocation()
+  {
+    if (stmt.loc)
+      return stmt.loc;
+    return f.loc;
+  }
+
+  function processVar(v)
+  {
+    // v.fieldOf.type is a struct/class if this is a stack constructor
+    // it's a pointer if this is some other construction technique
+    if (v.isConstructor) {
+      if ((!v.fieldOf || !v.fieldOf.type.isPointer)) {
+	if (v.fieldOf &&
+	    v.fieldOf.fieldOf &&
+	    v.fieldOf.fieldOf.name == "this") {
+	  return;
+	}
+        let m = getStorageClass(v.memberOf).blockStack([]);
+        if (m)
+          error("Constructing GC class %s on the stack\n%s".format(v.memberOf.name, m),
+	    getLocation());
+      }
+    }
+  }
+  for each (stmt in stmts)
+    iter(processVar, stmt.statements);
+}
diff --git a/xpcom/analysis/stack.js b/xpcom/analysis/stack.js
--- a/xpcom/analysis/stack.js
+++ b/xpcom/analysis/stack.js
@@ -1,73 +1,5 @@ include("gcc_util.js");
 include("gcc_util.js");
 include("unstable/lazy_types.js");
-
-function process_type(c)
-{
-  if ((c.kind == 'class' || c.kind == 'struct') &&
-      !c.isIncomplete)
-    isStack(c);
-}
-
-function isStack(c)
-{
-  function calculate()
-  {
-    if (hasAttribute(c, 'NS_stack'))
-      return true;
-
-    for each (let base in c.bases)
-      if (isStack(base))
-        return true;
-
-    for each (let member in c.members) {
-      if (member.isFunction)
-        continue;
-
-      let type = member.type;
-      while (true) {
-        if (type === undefined)
-          break;
-
-        if (type.isArray) {
-          type = type.type;
-          continue;
-        }
-
-        if (type.typedef) {
-          if (hasAttribute(type, 'NS_stack'))
-            return true;
-
-          type = type.typedef;
-          continue;
-        }
-        break;
-      }
-
-      if (type === undefined) {
-        warning("incomplete type for member " + member + ".", member.loc);
-        continue;
-      }
-
-      if (type.isPointer || type.isReference)
-        continue;
-
-      if (!type.kind || (type.kind != 'class' && type.kind != 'struct'))
-        continue;
-
-      if (isStack(type))
-        return true;
-    }
-    return false;
-  }
-
-  if (c.isIncomplete)
-    throw Error("Can't get stack property for incomplete type.");
-
-  if (!c.hasOwnProperty('isStack'))
-    c.isStack = calculate();
-
-  return c.isStack;
-}
 
 function isVoidPtr(t)
 {
@@ -95,18 +27,18 @@ function operator_new_assign(stmt)
       let name = IDENTIFIER_POINTER(nameid);
       if (name == "operator new" || name == "operator new []") {
         // if this is placement-new, ignore it (should we issue a warning?)
-        let fncallobj = dehydra_convert(TREE_TYPE(fncall));
-        if (fncallobj.parameters.length == 2 &&
-            isVoidPtr(fncallobj.parameters[1]))
-          return null;
+        let fncallobj = dehydra_convert(fncall);
+        if (fncallobj.type.parameters.length == 2 &&
+            isVoidPtr(fncallobj.type.parameters[1]))
+          return [null, null];
 
-        return varDecl;
+        return [varDecl, fncallobj];
       }
     }
   }
   catch (e if e.TreeCheckError) { }
 
-  return null;
+  return [null, null];
 }
 
 function process_tree(fndecl)
@@ -129,7 +61,7 @@ function process_tree(fndecl)
       return location_of(DECL_SAVED_TREE(fndecl));
     }
     
-    function check_opnew_assignment(varDecl, stmt)
+    function check_opnew_assignment(varDecl, fncallobj, stmt)
     {
       if (TREE_CODE(stmt) != GIMPLE_MODIFY_STMT) {
         warning("operator new not followed by a GIMPLE_MODIFY_STMT: " + TREE_CODE(stmt), getLocation(stmt));
@@ -151,9 +83,70 @@ function process_tree(fndecl)
         return;
       }
       destType = destType.type;
+      if (destType.kind === undefined ||
+          (destType.kind != 'struct' &&
+           destType.kind != 'class'))
+        return;
 
-      if (isStack(destType))
-        error("constructed object of type '" + destType.name + "' not on the stack.", getLocation(stmt));
+      // Figure out what operator new is being called and determine whether
+      // it's a mallocation, GCAllocation, or GCFinalizedAllocation
+      let allocType;
+      if (fncallobj.memberOf === undefined) {
+        allocType = 'malloc';
+      }
+      else {
+        let cx = fncallobj.memberOf;
+        if (cx.name == 'MMgc::GCObject' ||
+            cx.name == 'XPCOMGCObject') {
+          allocType = 'gc';
+        }
+        else if (cx.name == 'MMgc::GCFinalizedObject' ||
+                 cx.name == 'XPCOMGCFinalizedObject') {
+          allocType = 'gcfinalized';
+        }
+        else if (cx.name == 'MMgc::GCRoot') {
+          // TODO: think about this more
+          allocType = 'gcfinalized';
+        }
+        else if (hasAttribute(fncallobj, 'NS_mallocator')) {
+          allocType = 'malloc';
+        }
+        else if (hasAttribute(fncallobj, 'NS_gcallocator')) {
+          allocType = 'gc';
+        }
+        else if (hasAttribute(fncallobj, 'NS_gcfinalizedallocator')) {
+          allocType = 'gcfinalized';
+        }
+        else {
+          // error("Call to un-annotated %s\n%s:   declared here".format(fncallobj.name, fncallobj.loc), getLocation(stmt));
+          allocType = 'malloc';
+        }
+      }
+
+      let m;
+      let sc = getStorageClass(destType);
+      switch (allocType) {
+      case 'malloc':
+        m = sc.blockMalloc([]);
+        if (m)
+          error("constructed object of type %s on the malloc heap\n%s".format(destType.name, m), getLocation(stmt));
+        break;
+      case 'gc':
+        m = sc.blockGC([]);
+        if (m)
+          error("constructed object of type %s on the GC heap\n%s".format(destType.name, m), getLocation(stmt));
+        m = sc.canSkipFinalization([]);
+        if (m)
+          error("constructed object of type %s without finalization\n%s".format(destType.name, m), getLocation(stmt));
+        break;
+      case 'gcfinalized':
+        m = sc.blockGC([]);
+        if (m)
+          error("constructed object of type %s on the GC heap\n%s".format(destType.name, m), getLocation(stmt));
+        break;
+      default:
+        throw Error("analysis error: allocType was unrecognized value '%s'".format(allocType));
+      }
     }
 
     if (TREE_CODE(t) != STATEMENT_LIST)
@@ -162,11 +155,12 @@ function process_tree(fndecl)
     // if we find a tuple of "operator new" / GIMPLE_MODIFY_STMT casting
     // the result of operator new to a pointer type
     let opnew = null;
+    let fncallobj;
     for (let stmt in iter_statement_list(t)) {
       if (opnew != null)
-        check_opnew_assignment(opnew, stmt);
+        check_opnew_assignment(opnew, fncallobj, stmt);
 
-      opnew = operator_new_assign(stmt);
+      [opnew, fncallobj] = operator_new_assign(stmt);
     }
 
     if (opnew != null)
diff --git a/xpcom/analysis/static-checking.js b/xpcom/analysis/static-checking.js
--- a/xpcom/analysis/static-checking.js
+++ b/xpcom/analysis/static-checking.js
@@ -1,3 +1,5 @@
+require({ version: '1.8' });
+
 /**
  * A script for GCC-dehydra to analyze the Mozilla codebase and catch
  * patterns that are incorrect, but which cannot be detected by a compiler. */
@@ -9,6 +11,65 @@ function treehydra_enabled() {
 function treehydra_enabled() {
   return this.hasOwnProperty('TREE_CODE');
 }
+
+String.prototype.format = function string_format() {
+  // there are two modes of operation... unnamed indices are read in order;
+  // named indices using %(name)s. The two styles cannot be mixed.
+  // Unnamed indices can be passed as either a single argument to this function,
+  // multiple arguments to this function, or as a single array argument
+  let curindex = 0;
+  let d;
+
+  if (arguments.length > 1) {
+    d = arguments;
+  }
+  else
+    d = arguments[0];
+
+  function r(s, key, type) {
+    let v;
+    if (key == "") {
+      if (curindex == -1)
+        throw Error("Cannot mix named and positional indices in string formatting.");
+
+      if (curindex == 0 && (!(d instanceof Object) || !(0 in d))) {
+        v = d;
+      }
+      else if (!(curindex in d))
+        throw Error("Insufficient number of items in format, requesting item %i".format(curindex));
+      else {
+        v = d[curindex];
+      }
+
+      ++curindex;
+    }
+    else {
+      key = key.slice(1, -1);
+      if (curindex > 0)
+        throw Error("Cannot mix named and positional indices in string formatting.");
+      curindex = -1;
+
+      if (!(key in d))
+        throw Error("Key '%s' not present during string substitution.".format(key));
+      v = d[key];
+    }
+    switch (type) {
+    case "s":
+      return v.toString();
+    case "r":
+      return uneval(v);
+    case "i":
+      return parseInt(v);
+    case "f":
+      return Number(v);
+    case "%":
+      return "%";
+    default:
+      throw Error("Unexpected format character '%s'.".format(type));
+    }
+  }
+  return this.replace(/%(\([^)]+\))?(.)/g, r);
+};
 
 include('unstable/getopt.js');
 [options, args] = getopt();
@@ -43,19 +104,26 @@ function process_type(c)
       module.process_type(c);
 }
 
-function hasAttribute(c, attrname)
+function hasAnyAttribute(c, attrlist)
 {
-  var attr;
-
   if (c.attributes === undefined)
     return false;
 
-  for each (attr in c.attributes)
-    if (attr.name == 'user' && attr.value[0] == attrname)
-      return true;
+  for each (let attr in c.attributes) {
+    if (attr.name == 'user' && attrlist.some(function(v) {
+      return v == attr.value[0];
+    }))
+      return attr.value[0];
+  }
 
   return false;
 }
+
+function hasAttribute(c, attrname)
+{
+  return hasAnyAttribute(c, [attrname]) != false;
+}
+
 function process_function(f, stmts)
 {
   for each (let module in modules)
@@ -83,3 +151,402 @@ function input_end()
     if (module.hasOwnProperty('input_end'))
       module.input_end();
 }
+
+function StorageReason(loc, message, prev)
+{
+  this.loc = loc;
+  this.message = message;
+  this.prev = prev;
+}
+StorageReason.prototype.toString = function()
+{
+  let loc = this.loc;
+  if (loc === undefined)
+    loc = "<unknown location>";
+
+  let str = "%s:   %s".format(loc, this.message);
+  if (this.prev)
+    str += "\n%s".format(this.prev);
+  return str;
+};
+
+/**
+ * The various Storage types have a common interface:
+ * blockStack(stack)
+ * blockGlobal(stack)
+ * blockMalloc(stack)
+ * blockGC(stack)
+ * safeFinalizer(stack)
+ * canSkipFinalization(stack)
+ *
+ * Each of these returns null if everything's ok, or a StorageReason
+ *
+ * Any sub-types are checked against the type stack to ensure we don't enter
+ * an iloop checking pointer classes.
+ */
+function checkstack(stack, i)
+{
+  return stack.some(function(si) si === i);
+}
+
+ValueStorageClass =  {
+  blockStack: function() { return null; },
+  blockGlobal: function() { return null; },
+  blockMalloc: function() { return null; },
+  blockGC: function() { return null; },
+  safeFinalizer: function() { return null; },
+  canSkipFinalization: function() { return null; },
+  toString: function() { return "{ValueStorageClass)"; }
+};
+
+function PointerStorageClass(type)
+{
+  this._type = type;
+}
+PointerStorageClass.prototype = {
+  blockNotManaged: function(stack) {
+    if (checkstack(stack, this._type.type))
+      return null;
+
+    stack = stack.concat(this._type);
+    let subtypeClass = getStorageClass(this._type.type, stack);
+    if (!subtypeClass.blockGC(stack)) {
+      let reason = subtypeClass.blockMalloc(stack);
+      if (reason)
+        return new StorageReason(this._type.type.loc, "pointer to", reason);
+    }
+    return null;
+  },
+
+  blockStack: function() { return null; },
+  blockGlobal: function(stack) { return this.blockNotManaged(stack); },
+  blockMalloc: function(stack) { return this.blockNotManaged(stack); },
+  blockGC: function() { return null; },
+  safeFinalizer: function() { return null; },
+  canSkipFinalization: function() { return null; },
+  toString: function() { return "PointerStorageClass(%s)".format(this._type); }
+};
+
+function ArrayStorageClass(type)
+{
+  this._type = type;
+}
+ArrayStorageClass.prototype = {
+  /* Just forward everything to the inner... */
+  _forward: function(fname, stack) {
+    stack = stack.concat(this._type);
+    return getStorageClass(this._type.type)[fname](stack);
+  },
+  blockStack: function(stack) {
+    return this._forward('blockStack', stack);
+  },
+  blockGlobal: function(stack) {
+    return this._forward('blockGlobal', stack);
+  },
+  blockMalloc: function(stack) {
+    return this._forward('blockMalloc', stack);
+  },
+  blockGC: function(stack) {
+    return this._forward('blockGC', stack);
+  },
+  safeFinalizer: function(stack) {
+    return this._forward('safeFinalizer', stack);
+  },
+  canSkipFinalization: function(stack) {
+    return this._forward('canSkipFinalization', stack);
+  },
+  toString: function() { return "ArrayStorageClass(%s)".format(this._type); }
+};
+
+function TypedefStorageClass(type)
+{
+  this._type = type;
+}
+TypedefStorageClass.prototype = {
+  _forward: function(fname, stack) {
+    stack = stack.concat(this._type);
+    return getStorageClass(this._type.typedef)[fname](stack);
+  },
+
+  blockStack: function(stack) {
+    let m = hasAnyAttribute(this._type, ["NS_GC"]);
+    if (m != false)
+      return new StorageReason(this._type.loc, "typedef %s marked with annotation %s".format(this._type.name, m));
+    return this._forward('blockStack', stack);
+  },
+
+  blockGlobal: function(stack) {
+    let m = hasAnyAttribute(this._type, ["NS_stack", "NS_managed", "NS_GC"]);
+    if (m != false)
+      return new StorageReason(this._type.loc, "typedef %s marked with annotation %s".format(this._type.name, m));
+    return this._forward('blockGlobal', stack);
+  },
+
+  blockMalloc: function(stack) {
+    let m = hasAnyAttribute(this._type, ["NS_stack", "NS_managed", "NS_GC"]);
+    if (m != false)
+      return new StorageReason(this._type.loc, "typedef %s marked with annotation %s".format(this._type.name, m));
+    return this._forward('blockMalloc', stack);
+  },
+
+  blockGC: function(stack) {
+    let m = hasAnyAttribute(this._type, ["NS_stack", "NS_NoGC"]);
+    if (m != false)
+      return new StorageReason(this._type.loc, "typedef %s marked with annotation %s".format(this._type.name, m));
+    return this._forward('blockGC', stack);
+  },
+
+  safeFinalizer: function(stack) {
+    return this._forward('safeFinalizer', stack);
+  },
+  canSkipFinalization: function(stack) {
+    return this._forward('canSkipFinalization', stack);
+  },
+  toString: function() { return "TypedefStorageClass(%s)".format(this._type); }
+};
+
+/**
+ * Does a class have a declared destructor?
+ */
+function hasOwnDestructor(c)
+{
+  for each (let member in c.members) {
+    // dehydra version
+    if (member.isFunction && /::~/(member.name))
+      return true;
+
+    // treehydra version
+    if (member.isDestructor &&
+        !DECL_ARTIFICIAL(member._type))
+      return true;
+  }
+
+  return false;
+}
+
+function StructStorageClass(type)
+{
+  this._type = type;
+}
+StructStorageClass.prototype = {
+  /**
+   * @returns false or a StorageReason
+   * @note This function does not recurse, it's only for local use
+   */
+  isManaged: function() {
+    let m = hasAnyAttribute(this._type, ["NS_GC", "NS_managed"]);
+    if (m != false)
+      return new StorageReason(this._type.loc, "%s %s is annotated %s".format(this._type.kind, this._type.name, m));
+
+    if (this._type.name == 'MMgc::GCObject' ||
+        this._type.name == 'MMgc::GCFinalizable' ||
+        this._type.name == 'XPCOMGCObject' ||
+        this._type.name == 'XPCOMGCFinalizedObject')
+      return new StorageReason(this._type.loc, "%s %s is considered managed.".format(this._type.kind, this._type.name));
+
+    return null;
+  },
+
+  iterItems: function(fname, stack) {
+    if (this._type.isIncomplete)
+      return null;
+
+    stack = stack.concat(this._type);
+
+    for each (let base in this._type.bases) {
+      let r = getStorageClass(base)[fname](stack);
+      if (r != null)
+        return new StorageReason(this._type.loc, "%s %s is a base of %s %s".format(base.kind, base.name, this._type.kind, this._type.name), r);
+    }
+    for each (let member in this._type.members) {
+      if (member.isFunction || member.isStatic)
+        continue;
+
+      let r = getStorageClass(member)[fname](stack);
+      if (r != null)
+        return new StorageReason(this._type.loc, "in %s %s".format(this._type.kind, this._type.name),
+          new StorageReason(member.loc,
+                            "member %s".format(member.name, member.type), r));
+    }
+    return null;
+  },
+
+  blockStack: function(stack) {
+    let m = hasAnyAttribute(this._type, ["NS_GC"]);
+    if (m != false)
+      return new StorageReason(this._type.loc, "%s %s marked as %s".format(this._type.kind, this._type.name, m));
+    return this.iterItems('blockStack', stack);
+  },
+
+  blockGlobal: function(stack) {
+    let m = hasAnyAttribute(this._type, ["NS_stack"]);
+    if (m != false)
+      return new StorageReason(this._type.loc, "%s %s marked as %s".format(this._type.kind, this._type.name, m));
+
+    m = this.isManaged();
+    if (m != null)
+      return m;
+
+    return this.iterItems('blockGlobal', stack);
+  },
+
+  blockMalloc: function(stack) {
+    let m = hasAnyAttribute(this._type, ["NS_stack"]);
+    if (m != false) {
+      return new StorageReason(this._type.loc, "%s %s marked as %s".format(this._type.kind, this._type.name, m));
+    }
+
+    m = this.isManaged();
+    if (m != null)
+      return m;
+
+    return this.iterItems('blockMalloc', stack);
+  },
+
+  blockGC: function(stack) {
+    let m = hasAnyAttribute(this._type, ["NS_NoGC"]);
+    if (m != false)
+      return new StorageReason(this._type.loc, "%s %s marked as %s".format(this._type.kind, this._type.name, m));
+
+    return this.iterItems('blockGC', stack);
+  },
+
+  safeFinalizer: function(stack) {
+    let m = this.iterItems('safeFinalizer', stack);
+    if (m != null)
+      return m;
+
+    m = hasAnyAttribute(this._type, ["NS_GCOK"]);
+    if (m != false)
+      return null;
+
+    m = this.isManaged();
+    if (m != null)
+      return null; // Managed classes are finalizer-safe
+
+    if (hasOwnDestructor(this._type))
+      return new StorageReason(this._type.loc, "%s %s not annotated for as GC and has a destructor.".format(this._type.kind, this._type.name));
+
+    // If this class doesn't have a destructor, it's finalizer-safe.
+    // Implicit destructors are dealt with above in iterItems
+    return null;
+  },
+
+  canSkipFinalization: function(stack) {
+    let m = this.iterItems('canSkipFinalization', stack);
+    if (m != null)
+      return m;
+
+    if (hasAttribute(this._type, 'NS_finalizer_not_required'))
+      return null;
+
+    if (this._type.name == 'MMgc::GCObject' ||
+        this._type.name == 'MMgc::GCFinalizable' ||
+        this._type.name == 'XPCOMGCObject')
+      return null;
+
+    if (this._type.template &&
+        this._type.template.name == 'nsTArray_base' &&
+        this._type.template.arguments[0].name == 'GCAllocator')
+      return null;
+
+    if (this._type.template &&
+        this._type.template.name == 'nsTArray' &&
+        this._type.template.arguments[1].name == 'GCAllocator')
+      return null;
+
+    if (this._type.template &&
+	this._type.template.name == 'nsVoidArrayBase' &&
+	this._type.template.arguments[0].name == 'GCAllocator')
+      return null;
+
+    if (hasOwnDestructor(this._type))
+      return new StorageReason(this._type.loc, "%s %s has a destructor and is not annotated NS_FINALIZER_NOT_REQUIRED.".format(this._type.kind, this._type.name));
+
+    return null;
+  },
+  toString: function() { return "StructStorageClass(%s)".format(this._type); }
+};
+
+function DeclStorageClass(decl)
+{
+  this._decl = decl;
+}
+DeclStorageClass.prototype = {
+  blockStack: function(stack) {
+    return getStorageClass(this._decl.type).blockStack(stack);
+  },
+  blockGlobal: function(stack) {
+    if (hasAttribute(this._decl, "NS_unmanaged"))
+      return null;
+
+    return getStorageClass(this._decl.type).blockGlobal(stack);
+  },
+  blockMalloc: function(stack) {
+    if (hasAttribute(this._decl, "NS_unmanaged"))
+      return null;
+
+    return getStorageClass(this._decl.type).blockMalloc(stack);
+  },
+  blockGC: function(stack) {
+    return getStorageClass(this._decl.type).blockGC(stack);
+  },
+  safeFinalizer: function(stack) {
+    return getStorageClass(this._decl.type).safeFinalizer(stack);
+  },
+  canSkipFinalization: function(stack) {
+    return getStorageClass(this._decl.type).canSkipFinalization(stack);
+  },
+  toString: function() { return "DeclStorageClass(%s)".format(this._type); }
+};
+
+/**
+ * Get the storage class for a type.
+ */
+function getStorageClass(type)
+{
+  if (type.typedef)
+    return new TypedefStorageClass(type);
+
+  if (type.isPointer || type.isReference)
+    return new PointerStorageClass(type);
+
+  if (type.isArray)
+    return new ArrayStorageClass(type);
+
+  if (type.isFunction)
+    throw Error("How did a function decl end up in getStorageClass?");
+
+  if (type.kind) {
+    switch (type.kind) {
+    case "struct":
+    case "class":
+      return new StructStorageClass(type);
+    case "union":
+      // TODO: what the hell should union rules be? For now, bail!
+      return ValueStorageClass;
+    case "enum":
+      return ValueStorageClass;
+    default:
+      throw Error("Unrecognized type.kind '%s'".format(type.kind));
+    }
+  }
+
+  if (type.parameters) {
+    // this is a function *type*... we can end up here via function pointers
+    return ValueStorageClass;
+  }
+
+  if (type.precision || type.name == 'void') {
+    if (type.type)
+      throw Error("I thought this was a numeric type, but apparently it's not: %s".format(type));
+
+    return ValueStorageClass;
+  }
+
+  // If we're here, whatever we have is probably a decl...
+  if (type.type === undefined)
+    throw Error("Analysis error: %s should be a decl but doesn't have a type".format(type));
+
+  return new DeclStorageClass(type);
+}
diff --git a/xpcom/base/nscore.h b/xpcom/base/nscore.h
--- a/xpcom/base/nscore.h
+++ b/xpcom/base/nscore.h
@@ -497,9 +497,29 @@ typedef PRUint32 nsrefcnt;
 #ifdef NS_STATIC_CHECKING
 #define NS_STACK_CLASS __attribute__((user("NS_stack")))
 #define NS_FINAL_CLASS __attribute__((user("NS_final")))
+#define NS_MANAGED     __attribute__((user("NS_managed")))
+#define NS_GC_TYPE     __attribute__((user("NS_GC")))
+#define NS_NOGC_TYPE   __attribute__((user("NS_NoGC")))
+#define NS_FINALIZER_NOT_REQUIRED __attribute__((user("NS_finalizer_not_required")))
+#define NS_UNMANAGED   __attribute__((user("NS_unmanaged")))
+#define NS_FINALIZER   __attribute__((user("NS_finalizer")))
+#define NS_NO_FINALIZER __attribute__((user("NS_no_finalizer")))
+#define NS_MALLOCATOR  __attribute__((user("NS_mallocator")))
+#define NS_GCALLOCATOR __attribute__((user("NS_gcallocator")))
+#define NS_GCFINALIZEDALLOCATOR __attribute__((user("NS_gcfinalizedallocator")))
 #else
 #define NS_STACK_CLASS
 #define NS_FINAL_CLASS
+#define NS_MANAGED
+#define NS_GC_TYPE
+#define NS_NOGC_TYPE
+#define NS_FINALIZER_NOT_REQUIRED
+#define NS_UNMANAGED
+#define NS_FINALIZER
+#define NS_NO_FINALIZER
+#define NS_MALLOCATOR
+#define NS_GCALLOCATOR
+#define NS_GCFINALIZEDALLOCATOR
 #endif
 
 /**
diff --git a/xpcom/tests/static-checker/GCInheritance1.cpp b/xpcom/tests/static-checker/GCInheritance1.cpp
new file mode 100644
--- /dev/null
+++ b/xpcom/tests/static-checker/GCInheritance1.cpp
@@ -0,0 +1,11 @@
+#include "nsISupports.h"
+
+struct A
+{
+  int i;
+};
+
+// BAD: inherits from GCFinalizable but it's not the left-most base
+struct B : A, nsISupports
+{
+};
diff --git a/xpcom/tests/static-checker/GCInheritance2.cpp b/xpcom/tests/static-checker/GCInheritance2.cpp
new file mode 100644
--- /dev/null
+++ b/xpcom/tests/static-checker/GCInheritance2.cpp
@@ -0,0 +1,7 @@
+#include "nsISupportsImpl.h"
+
+// BAD: inherits from XPCOMGCFinalizedObject but GCFinalizable is not the
+// left-most base
+struct A : public XPCOMGCFinalizedObject
+{
+};
diff --git a/xpcom/tests/static-checker/GCNew.cpp b/xpcom/tests/static-checker/GCNew.cpp
new file mode 100644
--- /dev/null
+++ b/xpcom/tests/static-checker/GCNew.cpp
@@ -0,0 +1,13 @@
+#include "nscore.h"
+#include "nsISupportsUtils.h"
+
+struct NS_GC_TYPE A : public XPCOMGCObject
+{
+  A();
+  int i;
+};
+
+A* f()
+{
+  return new A();
+}
diff --git a/xpcom/tests/static-checker/GCNotStack-constructor.cpp b/xpcom/tests/static-checker/GCNotStack-constructor.cpp
new file mode 100644
--- /dev/null
+++ b/xpcom/tests/static-checker/GCNotStack-constructor.cpp
@@ -0,0 +1,17 @@
+#include "nscore.h"
+
+struct NS_GC_TYPE A
+{
+  A();
+  int i;
+};
+
+struct B : A
+{
+  B();
+};
+
+B::B()
+  : A()
+{
+}
diff --git a/xpcom/tests/static-checker/GCNotStack-constructor2.cpp b/xpcom/tests/static-checker/GCNotStack-constructor2.cpp
new file mode 100644
--- /dev/null
+++ b/xpcom/tests/static-checker/GCNotStack-constructor2.cpp
@@ -0,0 +1,20 @@
+#include "nscore.h"
+
+struct NS_GC_TYPE A
+{
+  A();
+  int i;
+};
+
+struct B : A
+{
+  B();
+};
+
+B::B()
+  : A()
+{
+  // This is a perverted case: you should be allowed to call A::A above on
+  // "this", but not separately!
+  A a;
+}
diff --git a/xpcom/tests/static-checker/GCNotStack-return.cpp b/xpcom/tests/static-checker/GCNotStack-return.cpp
new file mode 100644
--- /dev/null
+++ b/xpcom/tests/static-checker/GCNotStack-return.cpp
@@ -0,0 +1,16 @@
+#include "nscore.h"
+
+namespace GCNotStack_return {
+
+struct NS_GC_TYPE A
+{
+  A(int i);
+  int mI;
+};
+
+A f()
+{
+  return A(3);
+}
+
+}
diff --git a/xpcom/tests/static-checker/GCNotStack-static.cpp b/xpcom/tests/static-checker/GCNotStack-static.cpp
new file mode 100644
--- /dev/null
+++ b/xpcom/tests/static-checker/GCNotStack-static.cpp
@@ -0,0 +1,19 @@
+namespace GCStack_static
+{
+
+struct A
+{
+  int i;
+};
+
+int f(int i)
+{
+  static const A kA[] = {
+    { 1 },
+    { 2 }
+  };
+
+  return kA[i].i;
+}
+
+}
diff --git a/xpcom/tests/static-checker/GCNotStack.cpp b/xpcom/tests/static-checker/GCNotStack.cpp
new file mode 100644
--- /dev/null
+++ b/xpcom/tests/static-checker/GCNotStack.cpp
@@ -0,0 +1,16 @@
+#include "nscore.h"
+
+namespace GCNotStack {
+
+struct NS_GC_TYPE A
+{
+  A();
+  int i;
+};
+
+void f()
+{
+  A a;
+}
+
+} // namespace
diff --git a/xpcom/tests/static-checker/GCPointersManaged.cpp b/xpcom/tests/static-checker/GCPointersManaged.cpp
new file mode 100644
--- /dev/null
+++ b/xpcom/tests/static-checker/GCPointersManaged.cpp
@@ -0,0 +1,14 @@
+#include "nscore.h"
+
+struct NS_GC_TYPE A;
+
+struct B
+{
+  // pointers to GC types imply that class B is a managed type
+  A *a;
+};
+
+void f()
+{
+  B *b = new B();
+}
diff --git a/xpcom/tests/static-checker/Makefile.in b/xpcom/tests/static-checker/Makefile.in
--- a/xpcom/tests/static-checker/Makefile.in
+++ b/xpcom/tests/static-checker/Makefile.in
@@ -41,11 +41,28 @@ srcdir = @srcdir@
 srcdir = @srcdir@
 VPATH = @srcdir@
 
+MODULE = xpcom
+
 # MAKE_DIRS: List of directories to build while looping over directories.
 MAKE_DIRS               += $(MDDEPDIR)
 GARBAGE_DIRS            += $(MDDEPDIR)
 
 include $(DEPTH)/config/autoconf.mk
+
+GC_FAILURE_TESTCASES = \
+  GCNotStack.cpp \
+  GCNotStack-constructor2.cpp \
+  GCNotStack-return.cpp \
+  GCInheritance1.cpp \
+  GCInheritance2.cpp \
+  GCPointersManaged.cpp \
+  $(NULL)
+
+GC_PASS_TESTCASES = \
+  GCNew.cpp \
+  GCNotStack-constructor.cpp \
+  GCNotStack-static.cpp \
+  $(NULL)
 
 FINAL_FAILURE_TESTCASES = \
   TestFinal.cpp \
@@ -99,6 +116,7 @@ OUTPARAMS_PASS_TESTCASES = \
   $(NULL)
 
 STATIC_FAILURE_TESTCASES = \
+  $(GC_FAILURE_TESTCASES) \
   $(FINAL_FAILURE_TESTCASES) \
   $(STACK_FAILURE_TESTCASES) \
   $(NULL)
@@ -110,6 +128,7 @@ STATIC_PASS_TESTCASES = \
 STATIC_PASS_TESTCASES = \
   $(OUTPARAMS_PASS_TESTCASES) \
   $(STACK_PASS_TESTCASES) \
+  $(GC_PASS_TESTCASES) \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk