aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/syntax/FormatParser.java
blob: b5ae740c064bbdc0293e593024bc25fe2bdacb14 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
// Copyright 2014 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.syntax;

import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.events.Location;

import java.util.List;
import java.util.Map;

/**
 * A helper class that offers a subset of the functionality of Python's string#format.
 *
 * <p> Currently, both manual and automatic positional as well as named replacement
 * fields are supported. However, nested replacement fields are not allowed.
 */
public final class FormatParser {

  private static final ImmutableSet<Character> ILLEGAL_IN_FIELD =
      ImmutableSet.of('.', '[', ']', ',');

  private final Location location;

  public FormatParser(Location location) {
    this.location = location;
  }

  /**
   * Formats the given input string by using the given arguments
   *
   * <p>This method offers a subset of the functionality of Python's string#format
   *
   * @param input The string to be formatted
   * @param args Positional arguments
   * @param kwargs Named arguments
   * @return The formatted string
   */
  public String format(String input, List<Object> args, Map<String, Object> kwargs)
      throws EvalException {
    char[] chars = input.toCharArray();
    StringBuilder output = new StringBuilder();
    History history = new History();

    for (int pos = 0; pos < chars.length; ++pos) {
      char current = chars[pos];
      int advancePos = 0;

      if (current == '{') {
        advancePos = processOpeningBrace(chars, pos, args, kwargs, history, output);
      } else if (current == '}') {
        advancePos = processClosingBrace(chars, pos, output);
      } else {
        output.append(current);
      }

      pos += advancePos;
    }

    return output.toString();
  }

  /**
   * Processes the expression after an opening brace (possibly a replacement field) and emits
   * the result to the output StringBuilder
   *
   * @param chars The entire string
   * @param pos The position of the opening brace
   * @param args List of positional arguments
   * @param kwargs Map of named arguments
   * @param history Helper object that tracks information about previously seen positional
   *    replacement fields
   * @param output StringBuilder that consumes the result
   * @return Number of characters that have been consumed by this method
   */
  protected int processOpeningBrace(
      char[] chars,
      int pos,
      List<Object> args,
      Map<String, Object> kwargs,
      History history,
      StringBuilder output)
      throws EvalException {
    if (has(chars, pos + 1, '{')) {
      // Escaped brace -> output and move to char after right brace
      output.append("{");
      return 1;
    }

    // Inside a replacement field
    String key = getFieldName(chars, pos);
    Object value = null;

    // Only positional replacement fields will lead to a valid index
    try {
      int index = parsePositional(key, history);

      if (index < 0 || index >= args.size()) {
        fail("No replacement found for index " + index);
      }

      value = args.get(index);
    } catch (NumberFormatException nfe) {
      // Non-integer index -> Named
      if (!kwargs.containsKey(key)) {
        fail("Missing argument '" + key + "'");
      }

      value = kwargs.get(key);
    }

    // Format object for output
    output.append(Printer.str(value));

    // Advances the current position to the index of the closing brace of the
    // replacement field. Due to the definition of the enclosing for() loop,
    // the next iteration will examine the character right after the brace.
    return key.length() + 1;
  }

  /**
   * Processes a closing brace and emits the result to the output StringBuilder
   *
   * @param chars The entire string
   * @param pos Position of the closing brace
   * @param output StringBuilder that consumes the result
   * @return Number of characters that have been consumed by this method
   */
  protected int processClosingBrace(char[] chars, int pos, StringBuilder output)
      throws EvalException {
    if (!has(chars, pos + 1, '}')) {
      // Invalid brace outside replacement field
      fail("Found '}' without matching '{'");
    }

    // Escaped brace -> output and move to char after right brace
    output.append("}");
    return 1;
  }

  /**
   * Checks whether the given input string has a specific character at the given location
   *
   * @param data Input string as character array
   * @param pos Position to be checked
   * @param needle Character to be searched for
   * @return True if string has the specified character at the given location
   */
  protected boolean has(char[] data, int pos, char needle) {
    return pos < data.length && data[pos] == needle;
  }

  /**
   * Extracts the name/index of the replacement field that starts at the specified location
   *
   * @param chars Input string
   * @param openingBrace Position of the opening brace of the replacement field
   * @return Name or index of the current replacement field
   */
  protected String getFieldName(char[] chars, int openingBrace) throws EvalException {
    StringBuilder result = new StringBuilder();
    boolean foundClosingBrace = false;

    for (int pos = openingBrace + 1; pos < chars.length; ++pos) {
      char current = chars[pos];

      if (current == '}') {
        foundClosingBrace = true;
        break;
      } else {
        if (current == '{') {
          fail("Nested replacement fields are not supported");
        } else if (ILLEGAL_IN_FIELD.contains(current)) {
          fail("Invalid character '" + current + "' inside replacement field");
        }

        result.append(current);
      }
    }

    if (!foundClosingBrace) {
      fail("Found '{' without matching '}'");
    }

    return result.toString();
  }

  /**
   * Converts the given key into an integer or assigns the next available index, if empty.
   *
   * @param key Key to be converted
   * @param history Helper object that tracks information about previously seen positional
   *    replacement fields
   * @return The integer equivalent of the key
   */
  protected int parsePositional(String key, History history)
      throws NumberFormatException, EvalException {
    int result = -1;

    try {
      if (key.isEmpty()) {
        // Automatic positional -> a new index value has to be assigned
        history.setAutomaticPositional();
        result = history.getNextPosition();
      } else {
        // This will fail if key is a named argument
        result = Integer.parseInt(key);
        history.setManualPositional(); // Only register if the conversion succeeds
      }
    } catch (MixedTypeException mte) {
      fail(mte.getMessage());
    }

    return result;
  }

  /**
   * Throws an exception with the specified error message
   * @param msg The message to be thrown
   */
  protected void fail(String msg) throws EvalException {
    throw new EvalException(location, msg);
  }

  /**
   * Exception for invalid combinations of replacement field types
   */
  private static final class MixedTypeException extends Exception {
    public MixedTypeException() {
      super("Cannot mix manual and automatic numbering of positional fields");
    }
  }

  /**
   * A wrapper to keep track of information about previous replacement fields
   */
  private static final class History {
    /**
     * Different types of positional replacement fields
     */
    private enum Positional {
      NONE,
      MANUAL, // {0}, {1} etc.
      AUTOMATIC // {}
    }

    private Positional type = Positional.NONE;
    private int position = -1;

    /**
     * Returns the next available index for an automatic positional replacement field
     * @return Next index
     */
    public int getNextPosition() {
      ++position;
      return position;
    }

    /**
     * Registers a manual positional replacement field
     */
    public void setManualPositional() throws MixedTypeException {
      setPositional(Positional.MANUAL);
    }

    /**
     * Registers an automatic positional replacement field
     */
    public void setAutomaticPositional() throws MixedTypeException {
      setPositional(Positional.AUTOMATIC);
    }

    /**
     * Indicates that a positional replacement field of the specified type is being
     * processed and checks whether this conflicts with any previously seen
     * replacement fields
     *
     * @param current Type of current replacement field
     */
    protected void setPositional(Positional current) throws MixedTypeException {
      if (type == Positional.NONE) {
        type = current;
      } else if (type != current) {
        throw new MixedTypeException();
      }
    }
  }
}