blob: 100cce54da685a5523c0cb9aae5a3f84dd169a2f [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.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.devtools.build.lib.util.Pair;
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.TreeMap;
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 Map<String, List<Pair<Integer, NinjaVariableValue>>> variables;
private Map<String, List<Pair<Integer, String>>> expandedVariables;
private final Map<String, List<Pair<Integer, NinjaRule>>> rules;
private boolean isExpanded;
public NinjaScope() {
this(null, null);
}
public NinjaScope(@Nullable NinjaScope parentScope, @Nullable Integer includePoint) {
this.parentScope = parentScope;
this.includePoint = includePoint;
includedScopes = Maps.newTreeMap();
variables = Maps.newHashMap();
expandedVariables = Maps.newHashMap();
rules = Maps.newHashMap();
}
private NinjaScope(
@Nullable NinjaScope parentScope,
@Nullable Integer includePoint,
ImmutableSortedMap<String, List<Pair<Integer, String>>> expandedVariables) {
this.parentScope = parentScope;
this.includePoint = includePoint;
this.includedScopes = Maps.newTreeMap();
this.variables = Maps.newHashMap();
this.expandedVariables = expandedVariables;
this.rules = Maps.newHashMap();
this.isExpanded = true;
}
NinjaScope createTargetsScope(
ImmutableSortedMap<String, List<Pair<Integer, String>>> expandedVariables) {
return new NinjaScope(this, Integer.MAX_VALUE, expandedVariables);
}
public NinjaScope createIncludeScope(int offset) {
Preconditions.checkState(!isExpanded);
NinjaScope includeScope = new NinjaScope(this, offset);
includedScopes.put(offset, includeScope);
return includeScope;
}
public void addVariable(String name, int offset, NinjaVariableValue value) {
Preconditions.checkState(!isExpanded);
variables.computeIfAbsent(name, k -> Lists.newArrayList()).add(Pair.of(offset, value));
}
public void addRule(int offset, NinjaRule rule) {
Preconditions.checkState(!isExpanded);
rules.computeIfAbsent(rule.getName(), k -> Lists.newArrayList()).add(Pair.of(offset, rule));
}
@VisibleForTesting
public Map<String, List<Pair<Integer, NinjaVariableValue>>> getVariables() {
return variables;
}
@VisibleForTesting
public Map<String, List<Pair<Integer, NinjaRule>>> getRules() {
return rules;
}
@VisibleForTesting
public void sortResults() {
for (List<Pair<Integer, NinjaVariableValue>> list : variables.values()) {
list.sort(Comparator.comparing(Pair::getFirst));
}
for (List<Pair<Integer, NinjaRule>> list : rules.values()) {
list.sort(Comparator.comparing(Pair::getFirst));
}
}
/**
* 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.
*
* <p>This method is used either on already expanded scope, or in the process of the scope
* expansion in {@link #expandVariables}.
*/
public String getExpandedValue(int offset, NinjaVariableValue value) {
Preconditions.checkState(isExpanded);
// 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);
}
/** Resolve variables inside this scope and included scopes. */
public void expandVariables() {
Preconditions.checkState(!isExpanded);
/* To allow assertion in {@link #getExpandedValue} to pass. */
isExpanded = true;
TreeMap<Integer, Runnable> resolvables = Maps.newTreeMap();
for (Map.Entry<String, List<Pair<Integer, NinjaVariableValue>>> entry : variables.entrySet()) {
String name = entry.getKey();
for (Pair<Integer, NinjaVariableValue> pair : entry.getValue()) {
int offset = Preconditions.checkNotNull(pair.getFirst());
NinjaVariableValue variableValue = Preconditions.checkNotNull(pair.getSecond());
resolvables.put(
offset,
() ->
expandedVariables
.computeIfAbsent(name, k -> Lists.newArrayList())
.add(Pair.of(offset, getExpandedValue(offset, variableValue))));
}
}
for (Map.Entry<Integer, NinjaScope> entry : includedScopes.entrySet()) {
resolvables.put(entry.getKey(), () -> entry.getValue().expandVariables());
}
resolvables.values().forEach(Runnable::run);
expandedVariables = ImmutableSortedMap.copyOf(expandedVariables);
variables.clear();
}
/**
* Finds a 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 NinjaVariableValue findVariable(int offset, String name) {
return findByNameAndOffsetRecursively(offset, name, scope -> scope.variables);
}
/**
* 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 static NinjaScope mergeScopeParts(Collection<NinjaScope> parts) {
Preconditions.checkState(!parts.isEmpty());
NinjaScope first = Preconditions.checkNotNull(Iterables.getFirst(parts, null));
NinjaScope result = new NinjaScope(first.parentScope, first.includePoint);
for (NinjaScope part : parts) {
for (String name : part.variables.keySet()) {
result
.variables
.computeIfAbsent(name, k -> Lists.newArrayList())
.addAll(part.variables.get(name));
}
for (String name : part.rules.keySet()) {
result.rules.computeIfAbsent(name, k -> Lists.newArrayList()).addAll(part.rules.get(name));
}
}
result.sortResults();
return result;
}
}