| // 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()); |
| } |
| } |
| } |