001package daikon.split;
002
003import java.io.File;
004import java.io.FileNotFoundException;
005import java.io.IOException;
006import java.io.LineNumberReader;
007import java.util.ArrayList;
008import java.util.List;
009import jtb.ParseException;
010import org.checkerframework.checker.initialization.qual.UnknownInitialization;
011import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
012import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
013import org.checkerframework.checker.nullness.qual.Nullable;
014import org.checkerframework.checker.nullness.qual.RequiresNonNull;
015import org.checkerframework.dataflow.qual.Pure;
016import org.plumelib.util.FilesPlume;
017
018/**
019 * SpinfoFile stores information parsed from a {@code .spinfo} file. The constructor parses the
020 * file; then clients can make calls to retrieve the parsed information.
021 *
022 * <p>This file uses the term "ppt section" and "replace section" to describe what is refered to as
023 * a "Program Point Section" and "replacement sections" in the Daikon User Manual, respectively. A
024 * "ppt statement" is a single line from a "ppt section".
025 */
026public class SpinfoFile {
027
028  /** The path of the file being parsed. This is used only for debugging output. */
029  private String spinfoFileName;
030
031  /** The directory in which the java files for the splitters are kept. */
032  private String tempDir;
033
034  /** A Replacer constructed out of the replace statements from the spinfo file. */
035  // It is set by parseFile(), which is called by the constructor.
036  private StatementReplacer statementReplacer;
037
038  /**
039   * splitterObjects is an array of arrays of SplitterObjects where each array of SplitterObjects
040   * contains all the SplitterObjects for a single Ppt.
041   */
042  // It is set by parseFile(), which is called by the constructor.
043  // Invariant: for all i, j, and k:
044  // splitterObject[i][j].getPptName() == splitterObject[i][k].getPptName()
045  // splitterObject[i][k].getPptName() != splitterObject[j][k].getPptName() || i = j
046  private SplitterObject[][] splitterObjects;
047
048  private static String lineSep = System.lineSeparator();
049
050  /**
051   * Parses file spinfoFile.
052   *
053   * @param spinfoFile the file to be parsed
054   * @param tempDir the directory in which the splitters' java files are created
055   */
056  SpinfoFile(File spinfoFile, String tempDir) {
057    this.tempDir = tempDir;
058    this.spinfoFileName = spinfoFile.toString();
059    try (LineNumberReader reader = FilesPlume.newLineNumberFileReader(spinfoFile)) {
060      parseFile(reader);
061    } catch (FileNotFoundException e) {
062      throw new RuntimeException(e);
063    } catch (IOException e) {
064      throw new RuntimeException(e);
065    }
066  }
067
068  /**
069   * Gets the StatementReplacer for the replace statements in the file parsed.
070   *
071   * @return the Replacer for the replace statements in the file parsed
072   */
073  public StatementReplacer getReplacer() {
074    return statementReplacer;
075  }
076
077  /**
078   * Gets the SplitterObjects for the file parsed. For each Ppt in the spinfo file, one array of
079   * SplitterObjects is created. This method returns an array of those arrays.
080   *
081   * @return the SplitterObjects for the file parsed
082   */
083  public SplitterObject[][] getSplitterObjects() {
084    return splitterObjects;
085  }
086
087  /** Return the number of splitters (SplitterObject objects) represented by this file. */
088  public int numSplittterObjects() {
089    int result = 0;
090    for (SplitterObject[] spa : splitterObjects) {
091      result += spa.length;
092    }
093    return result;
094  }
095
096  /**
097   * Return the number of splitters (SplitterObject objects) represented by all the files in the
098   * list.
099   */
100  public static int numSplittterObjects(List<SpinfoFile> spinfoFiles) {
101    int result = 0;
102    for (SpinfoFile spf : spinfoFiles) {
103      result += spf.numSplittterObjects();
104    }
105    return result;
106  }
107
108  /**
109   * parseFile sets the member fields statementReplacer and splitterObjects, from the spinfoFile.
110   *
111   * @param spinfoFile a LineNumberReader for the spinfo file being parsed
112   * @throws IOException if an I/O error occurs
113   */
114  @RequiresNonNull("tempDir")
115  @EnsuresNonNull({"statementReplacer", "splitterObjects"})
116  public void parseFile(@UnknownInitialization SpinfoFile this, LineNumberReader spinfoFile)
117      throws IOException {
118    List<ReplaceStatement> replaceStatements = new ArrayList<>();
119    List<List<String>> pptSections = new ArrayList<>();
120    try {
121      String line = spinfoFile.readLine();
122      while (line != null) {
123        line = line.trim();
124        if (isComment(line) || line.equals("")) {
125          // nothing to do
126        } else if (line.equals("REPLACE")) {
127          readReplaceStatements(spinfoFile, replaceStatements);
128        } else if (line.startsWith("PPT_NAME")) {
129          line = line.substring("PPT_NAME".length()).trim();
130          readPptStatements(spinfoFile, pptSections, line);
131        } else {
132          throw new RuntimeException(
133              "Illegal file format in: "
134                  + spinfoFileName
135                  + lineSep
136                  + "at: "
137                  + spinfoFile.getLineNumber()
138                  + lineSep
139                  + line);
140        }
141        line = spinfoFile.readLine();
142      }
143    } catch (IOException ioe) {
144      //  System.err.println(ioe);
145      System.err.println("Error in " + spinfoFileName);
146      System.err.println(" at line number " + spinfoFile.getLineNumber() + " of .spinfo file");
147      throw new RuntimeException(ioe);
148    } catch (ParseException e) {
149      //  System.err.println(ioe);
150      System.err.println("Error in " + spinfoFileName);
151      System.err.println(" at line number " + spinfoFile.getLineNumber() + " of .spinfo file");
152      throw new RuntimeException(e);
153    }
154    statementReplacer = new StatementReplacer(replaceStatements);
155    splitterObjects = createSplitterObjects(pptSections);
156  }
157
158  /**
159   * Reads a group of replace statement lines. The method declaration and the return statement of a
160   * replace statement is placed in a ReplaceStatement. The ReplaceStatements are then placed into
161   * replaceStatements.
162   *
163   * @param spinfoFile a LineNumberReader for the spinfo file being parsed
164   * @param replaceStatements the List into which the ReplaceStatements are added
165   * @throws IOException if there is a problem reading the file
166   * @throws ParseException if there is a problem parsing
167   */
168  private void readReplaceStatements(
169      @UnknownInitialization SpinfoFile this,
170      LineNumberReader spinfoFile,
171      List<ReplaceStatement> replaceStatements)
172      throws IOException, ParseException {
173    String methodDeclaration = spinfoFile.readLine();
174    while (!isBlank(methodDeclaration)) {
175      String returnStatement = spinfoFile.readLine();
176      if (isBlank(returnStatement)) {
177        throw new RuntimeException(
178            "MalFormed .spinfo file in: "
179                + spinfoFileName
180                + lineSep
181                + (spinfoFile.getLineNumber() - 1)
182                + lineSep
183                + methodDeclaration
184                + lineSep
185                + "Each replace statement must be a pair of lines.");
186      }
187      ReplaceStatement replaceStatement =
188          new ReplaceStatement(methodDeclaration.trim(), returnStatement.trim());
189      replaceStatements.add(replaceStatement);
190      methodDeclaration = spinfoFile.readLine();
191    }
192  }
193
194  /**
195   * Reads a group of Ppt statements (statements following the line "PPT_NAME..."). Makes a list
196   * whose first element is the name on the PPT_NAME line (but without the "PPT_NAME" prefix), and
197   * whose additional elements are all the non-empty lines up to the next empty line. Puts this list
198   * in pptSections.
199   *
200   * @param spinfoFile a LineNumberReader for the spinfo file being parsed
201   * @param pptSections the List into which the List of lines for this pptSection are to be added
202   * @param pptName name of the ppt
203   * @throws IOException if an I/O error occurs
204   */
205  private void readPptStatements(
206      @UnknownInitialization SpinfoFile this,
207      LineNumberReader spinfoFile,
208      List<List<String>> pptSections,
209      String pptName)
210      throws IOException {
211    List<String> pptSection = new ArrayList<>();
212    pptSection.add(pptName);
213    String line = spinfoFile.readLine();
214    while ((line != null) && !line.trim().equals("")) {
215      pptSection.add(line);
216      line = spinfoFile.readLine();
217    }
218    pptSections.add(pptSection);
219  }
220
221  /**
222   * Creates the SplitterObjects for the Ppt section in pptSections.
223   *
224   * @param pptSections is a List of Lists of Strings. Each list should include all the lines from a
225   *     single Ppt Section. This includes the first line, but without the prefix "PPT_NAME"
226   * @return an array of arrays with each array containing the SplitterObjects for one of lists of
227   *     ppt statements found in pptSections
228   */
229  @RequiresNonNull("tempDir")
230  private SplitterObject[][] createSplitterObjects(
231      @UnknownInitialization SpinfoFile this, List<List<String>> pptSections) {
232    List<SplitterObject[]> splittersForAllPpts = new ArrayList<>();
233    for (List<String> pptSection : pptSections) {
234      List<SplitterObject> splittersForThisPpt = new ArrayList<>();
235      if (pptSection.size() > 0) {
236        String pptName = pptSection.get(0).trim();
237        SplitterObject splitObj = null;
238        for (int j = 1; j < pptSection.size(); j++) {
239          String pptStatement = pptSection.get(j);
240          if (isComment(pptStatement)) {
241            // nothing to do
242          } else if (isFormatting(pptStatement)) {
243            if (splitObj == null) {
244              throw new RuntimeException(
245                  "Malformed Spinfo file: "
246                      + spinfoFileName
247                      + lineSep
248                      + "Indented format specification, "
249                      + pptStatement
250                      + ", must follow an unindented condition"
251                      + lineSep
252                      + "For details, see the Daikon manual, section \"Splitter info file\"");
253            } else {
254              setFormatting(splitObj, pptStatement.trim());
255            }
256          } else {
257            splitObj = new SplitterObject(pptName, pptStatement.trim(), tempDir);
258            splittersForThisPpt.add(splitObj);
259          }
260        }
261      }
262      splittersForAllPpts.add(splittersForThisPpt.toArray(new SplitterObject[0]));
263    }
264    return splittersForAllPpts.toArray(new SplitterObject[0][0]);
265  }
266
267  /**
268   * Updates obj's fields to take in account the formatting command given by command. If the command
269   * is invalid an error message is given. Extra white space is ignored.
270   *
271   * @param obj the splitterObject for which command is intended
272   * @param command the formatting command to be applied to obj
273   */
274  private static void setFormatting(SplitterObject obj, String command) {
275    command = command.trim();
276    if (command.startsWith("DAIKON_FORMAT")) {
277      obj.daikonFormat = command.substring("DAIKON_FORMAT".length()).trim();
278      obj.dummyDesired = true;
279    } else if (command.startsWith("JAVA_FORMAT")) {
280      obj.javaFormat = command.substring("JAVA_FORMAT".length()).trim();
281      obj.dummyDesired = true;
282    } else if (command.startsWith("ESC_FORMAT")) {
283      obj.escFormat = command.substring("ESC_FORMAT".length()).trim();
284      obj.dummyDesired = true;
285    } else if (command.startsWith("SIMPLIFY_FORMAT")) {
286      obj.simplifyFormat = command.substring("SIMPLIFY_FORMAT".length()).trim();
287      obj.dummyDesired = true;
288    } else if (command.startsWith("CSHARPCONTRACT_FORMAT")) {
289      obj.csharpFormat = command.substring("CSHARPCONTRACT_FORMAT".length()).trim();
290      obj.dummyDesired = true;
291    } else {
292      System.err.println("Unrecognized format spec in .spinfo: " + command);
293    }
294  }
295
296  /** Returns whether the line is blank (or null). */
297  @EnsuresNonNullIf(result = false, expression = "#1")
298  @Pure
299  private static boolean isBlank(@Nullable String line) {
300    return (line == null) || line.trim().equals("");
301  }
302
303  /**
304   * Returns whether the line is a spinfo file comment line. A line is a comment if it starts with a
305   * (possibly indented) "#".
306   */
307  @Pure
308  private static boolean isComment(String line) {
309    return line.trim().startsWith("#");
310  }
311
312  /**
313   * Returns whether the line is a spinfo file formatting command. A line is a formatting command if
314   * line is indented with a tab ("\t") or spaces (" ").
315   */
316  @Pure
317  private static boolean isFormatting(String line) {
318    return line.startsWith("\t") || line.startsWith(" ");
319  }
320}