blob: ce8d0bdc651f614d29cbd7aedacb1d3ee62a557b [file] [log] [blame]
// Copyright 2019 The Bazel Authors. All rights reserved.
//
// 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.devtools.build.lib.bazel.rules.ninja.parser;
import static com.google.common.base.Strings.nullToEmpty;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.devtools.build.lib.util.Pair;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.annotation.Nullable;
/**
* Ninja file scope to keep all defined variables and rules according to the order of their
* definition (and redefinition).
*/
public class NinjaScope {
/** Parent scope for the case of subninja/include command */
@Nullable private final NinjaScope parentScope;
/** If include command was used for the current scope, the offset of that include command */
@Nullable private final Integer includePoint;
private final NavigableMap<Integer, NinjaScope> includedScopes;
private final NavigableMap<Integer, NinjaScope> subNinjaScopes;
private Map<String, List<Pair<Integer, String>>> expandedVariables;
private final Map<String, List<Pair<Integer, NinjaRule>>> rules;
public NinjaScope() {
this(null, null);
}
private NinjaScope(@Nullable NinjaScope parentScope, @Nullable Integer includePoint) {
this.parentScope = parentScope;
this.includePoint = includePoint;
this.rules = Maps.newTreeMap();
this.includedScopes = Maps.newTreeMap();
this.subNinjaScopes = Maps.newTreeMap();
this.expandedVariables = Maps.newHashMap();
}
public void setRules(Map<String, List<Pair<Integer, NinjaRule>>> rules) {
this.rules.putAll(rules);
}
@VisibleForTesting
public Map<String, List<Pair<Integer, NinjaRule>>> getRules() {
return rules;
}
public Collection<NinjaScope> getIncludedScopes() {
return includedScopes.values();
}
public Collection<NinjaScope> getSubNinjaScopes() {
return subNinjaScopes.values();
}
/**
* Expands variable value at the given offset. If some of the variable references, used in the
* value, can not be found, uses an empty string as their value.
*/
public String getExpandedValue(int offset, NinjaVariableValue value) {
// Cache expanded variables values to save time replacing several references to the same
// variable.
// This cache is local to the offset, it depends on the offset of the variable we are expanding.
Map<String, String> cache = Maps.newHashMap();
// We are using the start offset of the value holding the reference to the variable.
// Do the same as Ninja implementation: if the variable is not found, use empty string.
Function<String, String> expander =
ref -> cache.computeIfAbsent(ref, (key) -> nullToEmpty(findExpandedVariable(offset, key)));
return value.getExpandedValue(expander);
}
public void addExpandedVariable(int offset, String name, String value) {
expandedVariables.computeIfAbsent(name, k -> Lists.newArrayList()).add(Pair.of(offset, value));
}
public NinjaScope addIncluded(int offset) {
NinjaScope scope = new NinjaScope(this, offset);
includedScopes.put(offset, scope);
return scope;
}
public NinjaScope addSubNinja(int offset) {
NinjaScope scope = new NinjaScope(this, offset);
subNinjaScopes.put(offset, scope);
return scope;
}
/**
* Finds expanded variable with the name <code>name</code> to be used in the reference to it at
* <code>offset</code>. Returns null if nothing was found.
*/
@Nullable
public String findExpandedVariable(int offset, String name) {
return findByNameAndOffsetRecursively(offset, name, scope -> scope.expandedVariables);
}
/**
* Finds a rule with the name <code>name</code> to be used in the reference to it at <code>offset
* </code>. Returns null if nothing was found.
*/
@Nullable
public NinjaRule findRule(int offset, String name) {
return findByNameAndOffsetRecursively(offset, name, scope -> scope.rules);
}
/**
* Finds a variable or rule with the name <code>name</code> to be used in the reference to it at
* <code>offset</code>.
*
* <p>The following checks are made: - the last definition of variable/rule before the offset in
* the current scope is looked up. - the last definition of variable/rule inside the relevant
* included scopes (i.e. in the files from include statements before offset)
*
* <p>If any of the definitions are found in the current or included scopes, the value with the
* largest offset is returned.
*
* <p>If nothing is found, we make an attempt to find the definition in the parent scope at offset
* before the offset at which the current scope was introduced to parent.
*
* <p>If no definition was found, we return null.
*/
@Nullable
private <T> T findByNameAndOffsetRecursively(
int offset,
String name,
Function<NinjaScope, Map<String, List<Pair<Integer, T>>>> mapSupplier) {
Pair<Integer, T> currentScopeValue = findByNameAndOffset(offset, name, this, mapSupplier);
int currentScopeOffset =
currentScopeValue != null ? Preconditions.checkNotNull(currentScopeValue.getFirst()) : -1;
// Search in included scopes, which were included after the current scope, so they could
// override the value, but before the reference offset.
NavigableMap<Integer, NinjaScope> subMap =
includedScopes.subMap(currentScopeOffset, false, offset, false);
// Search in descending order, so that the first found value is the result.
for (NinjaScope includedScope : subMap.descendingMap().values()) {
T includedValue =
includedScope.findByNameAndOffsetRecursively(Integer.MAX_VALUE, name, mapSupplier);
if (includedValue != null) {
return includedValue;
}
}
if (currentScopeValue != null) {
return currentScopeValue.getSecond();
}
if (parentScope != null) {
Preconditions.checkNotNull(includePoint);
// -1 is used in order not to conflict with the current scope.
return parentScope.findByNameAndOffsetRecursively(includePoint - 1, name, mapSupplier);
}
return null;
}
/**
* Finds the variable or rule with the name <code>name</code>, defined in the current scope before
* the <code>offset</code>. (Ninja allows to re-define the values of rules and variables.)
*/
@Nullable
private static <T> Pair<Integer, T> findByNameAndOffset(
int offset,
String name,
NinjaScope scope,
Function<NinjaScope, Map<String, List<Pair<Integer, T>>>> mapFunction) {
List<Pair<Integer, T>> pairs = Preconditions.checkNotNull(mapFunction.apply(scope)).get(name);
if (pairs == null) {
// We may want to search in the parent scope.
return null;
}
int insertionPoint =
Collections.binarySearch(
pairs, Pair.of(offset, null), Comparator.comparing(Pair::getFirst));
if (insertionPoint >= 0) {
// Can not be, variable can not be defined in exactly same place.
throw new IllegalStateException("Trying to interpret declaration as reference.");
}
// We need to access the previous element, before the insertion point.
int idx = -insertionPoint - 2;
if (idx < 0) {
// Check the parent scope.
return null;
}
Pair<Integer, T> pair = pairs.get(idx);
return Pair.of(pair.getFirst(), pair.getSecond());
}
public NinjaScope createTargetsScope(
ImmutableSortedMap<String, List<Pair<Integer, String>>> expandedVariables) {
NinjaScope scope = new NinjaScope(this, Integer.MAX_VALUE);
scope.expandedVariables.putAll(expandedVariables);
return scope;
}
public void iterate(Consumer<NinjaScope> consumer) {
ArrayDeque<NinjaScope> queue = new ArrayDeque<>();
queue.add(this);
while (!queue.isEmpty()) {
NinjaScope currentScope = queue.removeFirst();
consumer.accept(currentScope);
queue.addAll(currentScope.getIncludedScopes());
queue.addAll(currentScope.getSubNinjaScopes());
}
}
}