diff --git a/ChangeLog b/ChangeLog
index 798df6c9829196ab917282b88c9d77c47a8571dc..f8539cc26c44d9285fd706375ea50d7475c02c97 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -7,6 +7,13 @@
     evaluated. The next time the attribute is accessed it checks if it can
     reuse the last cached value based on the current cycle state and the last
     cached cycle state.
+    * Added mechanism for custom control over the collection attribute survey
+    phase. This allows altering the tree traversal of collection attributes
+    to, e.g., traverse certain nonterminal nodes, or to exclude certain
+    subtrees. This feature works similar to an ordinary contribution
+    statement, but instead of a contribution expression the statement contains
+    a code block which is inserted directly into the generated collection
+    attribute tree traversal code.
 
 2016-03-16  Jesper Öqvist <jesper.oqvist@cs.lth.se>
 
diff --git a/doc/reference-manual.md b/doc/reference-manual.md
index 3c35810ce4f36b8c5c3be83e612756d144585ac1..0042d0d0b8aa6981636b70e3fb4ec371cebd1e1a 100644
--- a/doc/reference-manual.md
+++ b/doc/reference-manual.md
@@ -1460,6 +1460,28 @@ For example, if the collection attribute is declared as `coll
 LinkedList<String> ...` then `value-exp` should have the type
 `Iterable<String>`.
 
+#### Custom Collection Survey
+
+It is possible to customize the tree traversal used to search for contributions
+for a collection attribute. This can be done using an alternative form of the
+`contributes` statement, where the expression part is replaced by a code block:
+
+    N1 contributes {
+      getA().collectContributions();
+      super.collectContributions();
+    } to N2.a();
+
+
+The meaning of the above code is that the `N1` node type should search its `A`
+child while searching contributions for the `N2.a()` collection attribute. The
+call to `super.collectContributions()` is needed to ensure that all regular
+children of `N1` are also searched for contributions.
+
+Multiple custom collection survey blocks like this can be used, but only one of
+them needs to call `super.collectContributions()`. It is possible to use
+attributes inside the code blocks to decide when a particular subtree should be
+searched for contributions.
+
 ## <a id="Rewrites"></a>Rewrites
 
 JastAdd has a mechanism for replacing AST nodes by a rewritten version of the
diff --git a/src/jastadd/ast/CollectionAttributes.jrag b/src/jastadd/ast/CollectionAttributes.jrag
index 545f871930fb0cfc56b8335ccfd680babd59b2c9..09f600ef2d737b1d3c4d18ac56855b571b490650 100644
--- a/src/jastadd/ast/CollectionAttributes.jrag
+++ b/src/jastadd/ast/CollectionAttributes.jrag
@@ -159,8 +159,7 @@ aspect CollectionAttributes {
     return false;
   }
 
-  syn lazy String CollEq.contributionSignature() =
-      ((CollDecl) decl()).getTarget() + "_" + signature();
+  syn lazy String CollEq.contributionSignature() = decl().getTarget() + "_" + signature();
 
   /**
    * Lazy condition means that the condition is evaluated during the
@@ -201,56 +200,110 @@ aspect CollectionAttributes {
 
   syn boolean CollEq.hasCondition() = !getCondition().isEmpty();
 
+  interface SurveyContribution {
+    void emitSurveyCode(CollDecl delc, PrintStream out);
+  }
+
+  class EqSurveyContribution implements SurveyContribution {
+    private final CollEq eq;
+    public EqSurveyContribution(CollEq eq) {
+      this.eq = eq;
+    }
+
+    @Override
+    public void emitSurveyCode(CollDecl decl, PrintStream out) {
+      TemplateContext tt = eq.templateContext();
+      tt.bind("BottomValue", decl.getBottomValue());
+      tt.bind("CombOp", decl.getCombOp());
+      if (decl.onePhase()) {
+        tt.bind("HasCondition", eq.hasCondition());
+        tt.expand("CollEq.collectContributors:onePhase", out);
+      } else {
+        tt.bind("HasCondition", eq.hasCondition() && !decl.lazyCondition());
+        tt.expand("CollEq.collectContributors:twoPhase", out);
+      }
+    }
+  }
+
+  class BlockSurveyContribution implements SurveyContribution {
+    private final CustomSurveyBlock block;
+    public BlockSurveyContribution(CustomSurveyBlock block) {
+      this.block = block;
+    }
+
+    @Override
+    public void emitSurveyCode(CollDecl decl, PrintStream out) {
+      out.println(block.comment);
+      String replacement;
+      if (decl.onePhase()) {
+        replacement = String.format(".collect_contributors_%s(_root);", decl.collectionId());
+      } else {
+        replacement = String.format(".collect_contributors_%s(_root, _map);", decl.collectionId());
+      }
+      out.println(block.surveyCode.replaceAll("\\.collectContributions\\(\\);", replacement));
+    }
+  }
+
+  /**
+   * Generates the survey method for each type with a collection contribution.
+   */
   private void ASTDecl.collectContributors(PrintStream out) {
     // Mapping collection declaration to collection equations.
-    HashMap<CollDecl, Collection<CollEq>> map = new LinkedHashMap<CollDecl, Collection<CollEq>>();
+    HashMap<CollDecl, Collection<SurveyContribution>> map =
+        new LinkedHashMap<CollDecl, Collection<SurveyContribution>>();
 
-    for (int i = 0; i < getNumCollEq(); i++) {
-      CollEq attr = getCollEq(i);
-      CollDecl decl = (CollDecl) attr.decl();
-      Collection<CollEq> equations = map.get(decl);
+    for (CollEq attr : getCollEqList()) {
+      CollDecl decl = attr.decl();
+      Collection<SurveyContribution> equations = map.get(decl);
       if (equations == null) {
-        equations = new ArrayList<CollEq>();
+        equations = new ArrayList<SurveyContribution>();
         map.put(decl, equations);
       }
-      equations.add(attr);
+      equations.add(new EqSurveyContribution(attr));
     }
 
-    for (Map.Entry<CollDecl, Collection<CollEq>> entry : map.entrySet()) {
-      CollDecl decl = entry.getKey();
-      Collection<CollEq> equations = entry.getValue();
+    for (CustomSurveyBlock block : customSurveyBlocks) {
+      CollDecl decl = grammar().lookupCollDecl(block.collHost, block.collName);
+      if (decl == null) {
+        throw new Error(String.format(
+              "%s:%d: Can not add custom survey code for unknown collection attribute: %s.%s()",
+              block.fileName, block.startLine, block.collHost, block.collName));
+      }
+      Collection<SurveyContribution> equations = map.get(decl);
+      if (equations == null) {
+        equations = new ArrayList<SurveyContribution>();
+        map.put(decl, equations);
+      }
+      equations.add(new BlockSurveyContribution(block));
+    }
 
+    for (Map.Entry<CollDecl, Collection<SurveyContribution>> entry : map.entrySet()) {
+      CollDecl decl = entry.getKey();
+      Collection<SurveyContribution> equations = entry.getValue();
       decl.templateContext().expand("CollDecl.collectContributors:header", out);
-
-      String bottomValue = decl.getBottomValue();
-
-      for (CollEq equation : equations) {
-        TemplateContext tt = equation.templateContext();
-        tt.bind("BottomValue", bottomValue);
-        tt.bind("CombOp", decl.getCombOp());
-        if (decl.onePhase()) {
-          tt.bind("HasCondition", equation.hasCondition());
-          tt.expand("CollEq.collectContributors:onePhase", out);
-        } else {
-          tt.bind("HasCondition", equation.hasCondition() && !decl.lazyCondition());
-          tt.expand("CollEq.collectContributors:twoPhase", out);
-        }
+      boolean skipSuperCall = false;
+      for (SurveyContribution contribution : equations) {
+        contribution.emitSurveyCode(decl, out);
+        skipSuperCall |= contribution instanceof BlockSurveyContribution;
       }
-      if (isASTNodeDecl()) {
-        decl.templateContext().expand("CollDecl.collectContributors:default", out);
+      if (skipSuperCall) {
+        out.println(config().indent + "}");
       } else {
-        decl.templateContext().expand("CollDecl.collectContributors:end", out);
+        if (isASTNodeDecl()) {
+          decl.templateContext().expand("CollDecl.collectContributors:default", out);
+        } else {
+          decl.templateContext().expand("CollDecl.collectContributors:end", out);
+        }
       }
     }
   }
 
   private void ASTDecl.contributeTo(PrintStream out) {
-    HashMap<CollDecl, ArrayList<CollEq>> map = new LinkedHashMap<CollDecl, ArrayList<CollEq>>();
-    for (int i = 0; i < getNumCollEq(); i++) {
-      CollEq attr = getCollEq(i);
+    HashMap<CollDecl, Collection<CollEq>> map = new LinkedHashMap<CollDecl, Collection<CollEq>>();
+    for (CollEq attr : getCollEqList()) {
       if (!attr.onePhase()) {
-        CollDecl decl = (CollDecl) attr.decl();
-        ArrayList<CollEq> equations = map.get(decl);
+        CollDecl decl = attr.decl();
+        Collection<CollEq> equations = map.get(decl);
         if (equations == null) {
           equations = new ArrayList<CollEq>();
           map.put(decl, equations);
@@ -259,9 +312,9 @@ aspect CollectionAttributes {
       }
     }
 
-    for (Map.Entry<CollDecl, ArrayList<CollEq>> entry : map.entrySet()) {
+    for (Map.Entry<CollDecl, Collection<CollEq>> entry : map.entrySet()) {
       CollDecl decl = entry.getKey();
-      ArrayList<CollEq> equations = entry.getValue();
+      Collection<CollEq> equations = entry.getValue();
 
       decl.templateContext().bind("IsAstNode", isASTNodeDecl());
       decl.templateContext().expand("CollDecl.contributeTo:header", out);
@@ -392,13 +445,14 @@ aspect CollectionAttributes {
    * @return the collection attribute declaration, or {@code null} if no
    * declaration was found.
    */
-  syn CollDecl CollEq.decl() {
-    TypeDecl typeDecl = grammar().lookup(getTargetName());
+  syn CollDecl CollEq.decl() = grammar().lookupCollDecl(getTargetName(), getTargetAttributeName());
+
+  syn CollDecl Grammar.lookupCollDecl(String hostName, String collName) {
+    TypeDecl typeDecl = lookup(hostName);
     if (typeDecl != null) {
-      TypeDecl astDecl = (TypeDecl) typeDecl;
-      for (int i = 0; i < astDecl.getNumCollDecl(); i++) {
-        if (astDecl.getCollDecl(i).getName().equals(getTargetAttributeName())) {
-          return astDecl.getCollDecl(i);
+      for (CollDecl decl : typeDecl.getCollDeclList()) {
+        if (decl.getName().equals(collName)) {
+          return decl;
         }
       }
     }
@@ -462,26 +516,88 @@ aspect CollectionAttributes {
     return null;
   }
 
-  public CollEq Grammar.addCollEq(String targetName, String targetAttributeName,
-      String attributeType, String reference, String fileName,
+  public class CustomSurveyBlock {
+    public final String collName;
+    public final String collHost;
+    public final String surveyCode;
+    public final String fileName;
+    public final int startLine;
+    public final int endLine;
+    public final String comment;
+    public final String aspectName;
+
+    public CustomSurveyBlock(String collName, String collHost, String surveyCode,
+        String fileName, int startLine, int endLine, String comment,
+        String aspectName) {
+      this.collName = collName;
+      this.collHost = collHost;
+      this.surveyCode = surveyCode;
+      this.fileName = fileName;
+      this.startLine = startLine;
+      this.endLine = endLine;
+      this.comment = comment;
+      this.aspectName = aspectName;
+    }
+  }
+
+  public final Collection<CustomSurveyBlock> ASTDecl.customSurveyBlocks =
+      new LinkedList<CustomSurveyBlock>();
+
+  /**
+   * Adds code to the survey method for a particular collection attribute in the
+   * given node type.
+   */
+  public void Grammar.addCustomSurveyBlock(String collHost,
+      String collName,
+      String nodeType,
+      String codeBlock,
+      String fileName,
+      int startLine,
+      int endLine,
+      org.jastadd.jrag.AST.SimpleNode commentNode,
+      String aspectName,
+      ArrayList<String> annotations) {
+    TypeDecl type = lookup(nodeType);
+    if (type != null && type instanceof ASTDecl) {
+      ((ASTDecl) type).customSurveyBlocks.add(new CustomSurveyBlock(
+          collName,
+          collHost,
+          codeBlock,
+          fileName,
+          startLine,
+          endLine,
+          Unparser.unparseComment(commentNode),
+          aspectName));
+      for (String annotation : annotations) {
+        errorf("annotation %s not allowed for custom survey blocks.", annotation);
+      }
+    } else {
+      errorf("Can not add custom collection survey code to unknown class %s in %s at line %d",
+          nodeType, fileName, startLine);
+    }
+  }
+
+  public CollEq Grammar.addCollEq(String collHost,
+      String collName,
+      String nodeType, String reference, String fileName,
       int startLine, int endLine, boolean iterableValue, boolean iterableTarget,
-      org.jastadd.jrag.AST.SimpleNode node, String aspectName,
+      org.jastadd.jrag.AST.SimpleNode commentNode, String aspectName,
       ArrayList<String> annotations) {
-    TypeDecl c = lookup(attributeType);
-    if (c != null && c instanceof ASTDecl) {
+    TypeDecl type = lookup(nodeType);
+    if (type != null && type instanceof ASTDecl) {
       CollEq equ = new CollEq(
           new List(),
           new List(),
-          targetName,
+          collHost,
           fileName,
           startLine,
           endLine,
-          Unparser.unparseComment(node),
+          Unparser.unparseComment(commentNode),
           aspectName,
           "",
           "",
-          targetName,
-          targetAttributeName,
+          collHost,
+          collName,
           reference);
       equ.setIterableValue(iterableValue);
       equ.setIterableTarget(iterableTarget);
@@ -489,11 +605,11 @@ aspect CollectionAttributes {
         equ.addAnnotation(new Annotation(annotation));
       }
       // TODO(joqvist): defer collection attribute weaving.
-      ((ASTDecl) c).addCollEq(equ);
+      ((ASTDecl) type).addCollEq(equ);
       return equ;
     } else {
       errorf("Can not add collection contribution to unknown class %s in %s at line %d",
-          attributeType, fileName, startLine);
+          nodeType, fileName, startLine);
       // TODO(joqvist): defer weaving so we can return non-null.
       return null;
     }
diff --git a/src/javacc/jrag/Jrag.jjt b/src/javacc/jrag/Jrag.jjt
index 9bbcd42428a3c6e644f20c5dca99ed81e48fcbde..bd9a97408f0d87e68a75dba480ca25c40e24b26f 100644
--- a/src/javacc/jrag/Jrag.jjt
+++ b/src/javacc/jrag/Jrag.jjt
@@ -1371,11 +1371,13 @@ void CollectionContribution():
 {
   Token t;
   Token first, last;
-  SimpleNode value;
+  SimpleNode block;
+  SimpleNode value = null;
   SimpleNode condition = null;
   String targetName;
   String targetAttributeName;
   String attributeType;
+  String surveyBlock = null;
   SimpleNode reference = null;
   org.jastadd.ast.AST.List contributionList = new org.jastadd.ast.AST.List();
   boolean iterableValue = false;
@@ -1387,30 +1389,47 @@ void CollectionContribution():
   ( annotation = Annotation() { annotations.add(Unparser.unparseComment(annotation)); } )*
   t = <IDENTIFIER> { first = token; attributeType = t.image; }
   "contributes"
-  [ LOOKAHEAD("each") "each" { iterableValue = true; } ]
-  value = Expression()
-  [ "when" condition = Expression() ]
-  "to" t = <IDENTIFIER> "." { targetName = t.image; } t = AttributeName() { targetAttributeName = t.image; } "(" ")"
-  [ "for" [ LOOKAHEAD("each") "each" { iterableTarget = true; } ] reference = Expression() ]
+  (
+    [ LOOKAHEAD("each") "each" { iterableValue = true; } ]
+    value = Expression()
+    [ "when" condition = Expression() ]
+    "to" t = <IDENTIFIER> "." { targetName = t.image; } t = AttributeName() { targetAttributeName = t.image; } "(" ")"
+    [ "for" [ LOOKAHEAD("each") "each" { iterableTarget = true; } ] reference = Expression() ]
+  | block = Block() { surveyBlock = Unparser.unparse(block); }
+    "to" t = <IDENTIFIER> "." { targetName = t.image; } t = AttributeName() { targetAttributeName = t.image; } "(" ")"
+  )
   ";" { last = token; }
   {
-    org.jastadd.ast.AST.CollEq equ = root.addCollEq(targetName,
-        targetAttributeName,
-        attributeType,
-        reference == null ? "" : Unparser.unparse(reference),
-        fileName,
-        first.beginLine,
-        last.endLine,
-        iterableValue,
-        iterableTarget,
-        jjtThis,
-        enclosingAspect,
-        annotations);
-    // TODO(joqvist): make equ non-null.
-    if (equ != null) {
-      equ.setValue(Unparser.unparse(value));
-      if (condition != null) {
-          equ.setCondition(Unparser.unparse(condition));
+    if (surveyBlock != null) {
+      root.addCustomSurveyBlock(targetName,
+          targetAttributeName,
+          attributeType,
+          surveyBlock,
+          fileName,
+          first.beginLine,
+          last.endLine,
+          jjtThis,
+          enclosingAspect,
+          annotations);
+    } else {
+      org.jastadd.ast.AST.CollEq equ = root.addCollEq(targetName,
+          targetAttributeName,
+          attributeType,
+          reference == null ? "" : Unparser.unparse(reference),
+          fileName,
+          first.beginLine,
+          last.endLine,
+          iterableValue,
+          iterableTarget,
+          jjtThis,
+          enclosingAspect,
+          annotations);
+      // TODO(joqvist): make equ non-null.
+      if (equ != null) {
+        equ.setValue(Unparser.unparse(value));
+        if (condition != null) {
+            equ.setCondition(Unparser.unparse(condition));
+        }
       }
     }
   }