| // Copyright (C) 2017 The Android Open Source Project |
| // |
| // 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.googlesource.gerrit.plugins.findowners; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Parse lines in an OWNERS file and put them into an OwnersDb. |
| * |
| * <p>OWNERS file syntax: |
| * |
| * <pre> |
| * lines := (\s* line? \s* "\n")* |
| * line := "set" \s+ "noparent" |
| * | "per-file" \s+ globs \s* "=" \s* directives |
| * | "file:" \s* glob |
| * | comment |
| * | directive |
| * directives := directive (comma directive)* |
| * directive := email_address |
| * | "*" |
| * globs := glob (comma glob)* |
| * glob := [a-zA-Z0-9_-*?.]+ |
| * comma := \s* "," \s* |
| * comment := "#" [^"\n"]* |
| * </pre> |
| * |
| * <p>The "file:" directive is not implemented yet. |
| * |
| * <p>"per-file globs = directives" applies each directive to files matching any of the globs. |
| * A glob does not contain directory path. |
| */ |
| class Parser { |
| // Globs and emails are separated by commas with optional spaces around a comma. |
| protected static final String COMMA = "[\\s]*,[\\s]*"; // used in unit tests |
| |
| private static final String BOL = "^[\\s]*"; // begin-of-line |
| private static final String EOL = "[\\s]*(#.*)?$"; // end-of-line |
| private static final String GLOB = "[^\\s,=]+"; // a file glob |
| |
| // TODO: have a more precise email address pattern. |
| private static final String EMAIL_OR_STAR = "([^\\s<>@,]+@[^\\s<>@#,]+|\\*)"; |
| |
| // Simple input lines with 0 or 1 sub-pattern. |
| private static final Pattern patComment = Pattern.compile(BOL + EOL); |
| private static final Pattern patEmail = Pattern.compile(BOL + EMAIL_OR_STAR + EOL); |
| private static final Pattern patFile = Pattern.compile(BOL + "file:.*" + EOL); |
| private static final Pattern patNoParent = Pattern.compile(BOL + "set[\\s]+noparent" + EOL); |
| |
| // Patterns to match trimmed globs and emails in per-file lines. |
| private static final Pattern patEmailList = |
| Pattern.compile("^(" + EMAIL_OR_STAR + "(" + COMMA + EMAIL_OR_STAR + ")*)$"); |
| private static final Pattern patGlobs = |
| Pattern.compile("^(" + GLOB + "(" + COMMA + GLOB + ")*)$"); |
| // patPerFile matches a line to two groups: (1) globs, (2) emails |
| // Trimmed 1st group should match patGlobs; trimmed 2nd group should match patEmailList. |
| private static final Pattern patPerFile = |
| Pattern.compile(BOL + "per-file[\\s]+([^=#]+)=[\\s]*([^#]+)" + EOL); |
| |
| static boolean isComment(String line) { |
| return patComment.matcher(line).matches(); |
| } |
| |
| static boolean isFile(String line) { |
| return patFile.matcher(line).matches(); |
| } |
| |
| static boolean isGlobs(String line) { |
| return patGlobs.matcher(line).matches(); |
| } |
| |
| static boolean isNoParent(String line) { |
| return patNoParent.matcher(line).matches(); |
| } |
| |
| static String parseEmail(String line) { |
| Matcher m = Parser.patEmail.matcher(line); |
| return m.matches() ? m.group(1).trim() : null; |
| } |
| |
| static String[] parsePerFile(String line) { |
| Matcher m = patPerFile.matcher(line); |
| if (!m.matches() || !isGlobs(m.group(1).trim()) |
| || !patEmailList.matcher(m.group(2).trim()).matches()) { |
| return null; |
| } |
| String[] parts = new String[2]; |
| parts[0] = m.group(1).trim(); |
| parts[1] = m.group(2).trim(); |
| return parts; |
| } |
| |
| static String[] parsePerFileEmails(String line) { |
| String[] globsAndEmails = parsePerFile(line); |
| return (globsAndEmails != null) ? globsAndEmails[1].split(COMMA) : null; |
| } |
| |
| static class Result { |
| boolean stopLooking; // if this file contains set noparent |
| List<String> warnings; // warning messages |
| List<String> errors; // error messages |
| Map<String, Set<String>> owner2paths; // maps from owner email to pathGlobs |
| |
| Result() { |
| stopLooking = false; |
| warnings = new ArrayList<>(); |
| errors = new ArrayList<>(); |
| owner2paths = new HashMap<>(); |
| } |
| } |
| |
| static Result parseFile(String dir, String file, String[] lines) { |
| Result result = new Result(); |
| int n = 0; |
| for (String line : lines) { |
| Parser.parseLine(result, dir, file, line, ++n); |
| } |
| return result; |
| } |
| |
| /** |
| * Parse a line in OWNERS file and add info to OwnersDb. |
| * |
| * @param result a Result object to keep parsed info. |
| * @param dir the path to OWNERS file directory. |
| * @param file the OWNERS file path. |
| * @param line the source line. |
| * @param num the line number. |
| */ |
| static void parseLine(Result result, String dir, String file, String line, int num) { |
| // comment and file: directive are parsed but ignored. |
| String email; |
| String[] globsAndEmails; |
| if (isNoParent(line)) { |
| result.stopLooking = true; |
| } else if (isComment(line)) { |
| // ignore comment and empty lines. |
| } else if ((email = parseEmail(line)) != null) { |
| Util.addToMap(result.owner2paths, email, dir); |
| } else if ((globsAndEmails = parsePerFile(line)) != null) { |
| String[] emails = globsAndEmails[1].split(COMMA, -1); |
| for (String glob : globsAndEmails[0].split(COMMA, -1)) { |
| for (String e : emails) { |
| Util.addToMap(result.owner2paths, e, dir + glob); |
| } |
| } |
| } else if (isFile(line)) { |
| result.warnings.add(warningMsg(file, num, "ignored", line)); |
| } else { |
| result.errors.add(errorMsg(file, num, "ignored unknown line", line)); |
| } |
| } |
| |
| private static String createMsgLine(String prefix, String file, int n, String msg, String line) { |
| return prefix + file + ":" + n + ": " + msg + ": [" + line + "]"; |
| } |
| |
| static String errorMsg(String file, int n, String msg, String line) { |
| return createMsgLine("Error: ", file, n, msg, line); |
| } |
| |
| static String warningMsg(String file, int n, String msg, String line) { |
| return createMsgLine("Warning: ", file, n, msg, line); |
| } |
| } |