The compiler doesn't really implement "tree-shaking" but rather "tree-growing".
It starts by doing a fast diet-parsing which reads all the files and collects statics, classes and methods (without really looking into them). Then it finds the main-entry point `main` and resolves all identifiers. Using the data that was collected from the diet-parser it grows the reachable set of code. This is done in the enqueuer. Every method that is reached this way is then, in turn, resolved and the tree continues to grow until a fixed point is reached.
Note that in many cases dart2js has to be conservative: if it sees `o.foo` but doesn't exactly know what the type of `o` is, it needs to enqueue all possible `foo` methods. For this reason there is another tree-growing, when we actually build the code for every method. Starting, again, with `main` dart2js compiles every reachable method. At this point the generated code assumes that every marked class/method (from the resolution phase) is alive. However, it starts its own queue where it only puts elements (methods, classes, ...) that the code-generator itself determines to be reachable.
For example:
main() {
if (false) print(new A());
}
In the resolver the class `A` would be marked as reachable, since there is an instantiation of it in the code.
However, the code-gen would do dead-code elimination and remove the call to the constructor. As such it would not appear in the final output.
Hope that helps.