com.google.javascript.jscomp.testing.ScopeSubject Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler-unshaded Show documentation
Show all versions of closure-compiler-unshaded Show documentation
Closure Compiler is a JavaScript optimizing compiler. It parses your
JavaScript, analyzes it, removes dead code and rewrites and minimizes
what's left. It also checks syntax, variable references, and types, and
warns about common JavaScript pitfalls. It is used in many of Google's
JavaScript apps, including Gmail, Google Web Search, Google Maps, and
Google Docs.
/*
* Copyright 2015 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp.testing;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Fact.fact;
import static com.google.common.truth.Fact.simpleFact;
import static com.google.common.truth.Truth.assertAbout;
import static com.google.common.truth.Truth.assertThat;
import static com.google.javascript.rhino.testing.TypeSubject.assertType;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.Subject;
import com.google.javascript.jscomp.AbstractScope;
import com.google.javascript.jscomp.AbstractVar;
import com.google.javascript.jscomp.TypedVar;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.testing.TypeSubject;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
/**
* A Truth Subject for the AbstractScope class. Usage:
*
*
* import static com.google.javascript.jscomp.testing.ScopeSubject.assertScope;
* ...
* assertScope(scope).declares("somevar");
* assertScope(scope).declares("otherVar").directly();
* assertScope(scope).declares("yetAnotherVar").onClosestContainerScope();
*
*/
public final class ScopeSubject extends Subject {
@CheckReturnValue
public static ScopeSubject assertScope(AbstractScope scope) {
// NB: Eclipse's Java compiler bails on just passing ScopeSubject::new below, so wrap it in a
// Closure.
return assertAbout((FailureMetadata fm, AbstractScope s) -> new ScopeSubject(fm, s))
.that(scope);
}
private final AbstractScope actual;
private ScopeSubject(FailureMetadata failureMetadata, AbstractScope scope) {
super(failureMetadata, scope);
this.actual = scope;
}
public void doesNotDeclare(String name) {
AbstractVar var = getVar(name);
if (var != null) {
failWithoutActual(
fact("expected not to declare", name),
fact("but declared it with value", var),
fact("scope was", actual));
}
}
public DeclarationSubject declares(String name) {
AbstractVar var = getVar(name);
if (var == null) {
ImmutableList> declared =
ImmutableList.copyOf(actual.getAllAccessibleVariables());
ImmutableList names =
declared.stream().map(AbstractVar::getName).collect(toImmutableList());
if (names.size() > 10) {
names =
ImmutableList.builder()
.addAll(names.subList(0, 9))
.add("and " + (names.size() - 9) + " others")
.build();
}
failWithoutActual(
fact("expected to declare", name),
simpleFact("but did not"),
fact("did declare", Joiner.on(", ").join(names)),
fact("scope was", actual));
}
assertThat(actual.getTopmostScopeOfEventualDeclaration(name)).isEqualTo(var.getScope());
return new DeclarationSubject(var);
}
private AbstractVar getVar(String name) {
return actual.hasSlot(name) ? checkNotNull(actual.getVar(name)) : null;
}
/** A subject for an {@link AbstractVar} declared by this particular scope. */
public final class DeclarationSubject {
private final AbstractVar var;
private DeclarationSubject(AbstractVar var) {
this.var = checkNotNull(var);
}
/**
* Expects the variable to be defined on the given {@code scope}. The {@code preposition} is
* either "on" or "", depending on whether it is needed for grammatical correctness. The {@code
* expected} object is displayed in brackets, in parallel to the actual scope that the variable
* is defined on.
*/
private void expectScope(String preposition, Object expected, AbstractScope scope) {
if (var.getScope() != scope) {
failWithoutActual(
fact("for var", var.getName()),
fact(
"expected to be declared" + (!preposition.isEmpty() ? " " : "") + preposition,
expected),
fact("but is declared on", var.getScope()));
}
}
/** Expects the declared variable to be declared on the subject scope. */
public DeclarationSubject directly() {
expectScope("", "directly", actual);
return this;
}
/** Expects the declared variable to be declared on the given scope. */
public DeclarationSubject on(AbstractScope scope) {
checkState(
scope != actual,
"It doesn't make sense to pass the scope already being asserted about. Use .directly()");
expectScope("on", scope, scope);
return this;
}
/** Expects the declared variable to be declared on the closest container scope. */
public DeclarationSubject onClosestContainerScope() {
expectScope("on", "the closest container scope", actual.getClosestContainerScope());
return this;
}
/** Expects the declared variable to be declared on the closest hoist scope. */
public DeclarationSubject onClosestHoistScope() {
expectScope("on", "the closest hoist scope", actual.getClosestHoistScope());
return this;
}
/** Expects the declared variable to be declared on the global scope. */
public DeclarationSubject globally() {
expectScope("", "globally", actual.getGlobalScope());
return this;
}
/** Expects the declared variable to be declared on any scope other than the subject. */
public DeclarationSubject onSomeParent() {
if (var != null && var.getScope() == actual) {
failWithoutActual(
fact("for var", var.getName()),
simpleFact("expected a declaration on a parent scope"),
simpleFact("but found it declared directly"),
fact("scope was", actual));
}
return this;
}
/** Expects the declared variable to be declared on some scope with the given label. */
public DeclarationSubject onScopeLabeled(String expectedLabel) {
checkNotNull(expectedLabel);
String actualLabel = getLabel(var.getScopeRoot());
if (actualLabel == null) {
failWithoutActual(
fact("expected to declare", var.getName()),
fact("on a scope labeled", expectedLabel),
simpleFact("but declared it on an unlabeled scope"),
fact("scope under test was", actual));
} else if (!actualLabel.equals(expectedLabel)) {
failWithoutActual(
fact("expected to declare", var.getName()),
fact("on a scope labeled", expectedLabel),
fact("but declared it on an scope labeled", actualLabel),
fact("scope under test was", actual));
}
return this;
}
public TypeSubject withTypeThat() {
TypedVar typedVar = (TypedVar) var.getSymbol();
return assertType(typedVar.getType());
}
}
/** Returns the name of the label applied to n, or null if none exists. */
@Nullable
private String getLabel(Node n) {
// If the node is labeled it will be the second child of a LABEL and the first child
// will be a LABEL_NAME.
Node parent = n.getParent();
if (parent != null && parent.isLabel()) {
Node labelNameNode = parent.getFirstChild();
checkState(labelNameNode.isLabelName(), labelNameNode);
checkState(labelNameNode.getNext() == n, n);
return labelNameNode.getString();
} else {
return null;
}
}
}