001package org.editorconfig.core; 002 003import java.io.*; 004import java.util.*; 005import java.util.regex.Matcher; 006import java.util.regex.Pattern; 007 008/** 009 * EditorConfig handler 010 * 011 * @author Dennis.Ushakov 012 */ 013public class EditorConfig { 014 public static String VERSION = "0.11.3-final"; 015 016 private static final Pattern SECTION_PATTERN = Pattern.compile("\\s*\\[(([^#;]|\\\\#|\\\\;)+)]" + 017 ".*"); // Python match searches from the line start 018 private static final int HEADER = 1; 019 020 private static final Pattern OPTION_PATTERN = Pattern.compile("\\s*([^:=\\s][^:=]*)\\s*[:=]\\s*(.*)"); 021 private static final int OPTION = 1; 022 private static final int VAL = 2; 023 024 private final String configFilename; 025 private final String version; 026 027 /** 028 * Creates EditorConfig handler with default configuration filename (.editorconfig) and 029 * version {@link EditorConfig#VERSION} 030 */ 031 public EditorConfig() { 032 this(".editorconfig", VERSION); 033 } 034 035 /** 036 * Creates EditorConfig handler with specified configuration filename and version. 037 * Used mostly for debugging/testing. 038 * @param configFilename configuration file name to be searched for instead of .editorconfig 039 * @param version required version 040 */ 041 public EditorConfig(String configFilename, String version) { 042 this.configFilename = configFilename; 043 this.version = version; 044 } 045 046 /** 047 * Parse editorconfig files corresponding to the file path given by filename, and return the parsing result. 048 * 049 * @param filePath The full path to be parsed. The path is usually the path of the file which is currently edited 050 * by the editor. 051 * @return The parsing result stored in a list of {@link EditorConfig.OutPair}. 052 * @throws org.editorconfig.core.ParsingException If an {@code .editorconfig} file could not be parsed 053 * @throws org.editorconfig.core.VersionException If version greater than actual is specified in constructor 054 * @throws org.editorconfig.core.EditorConfigException If an EditorConfig exception occurs. Usually one of 055 * {@link ParsingException} or {@link VersionException} 056 */ 057 public List<OutPair> getProperties(String filePath) throws EditorConfigException { 058 checkAssertions(); 059 Map<String, String> oldOptions = Collections.emptyMap(); 060 Map<String, String> options = new LinkedHashMap<String, String>(); 061 try { 062 boolean root = false; 063 String dir = new File(filePath).getParent(); 064 while (dir != null && !root) { 065 String configPath = dir + "/" + configFilename; 066 if (new File(configPath).exists()) { 067 FileInputStream stream = new FileInputStream(configPath); 068 InputStreamReader reader = new InputStreamReader(stream, "UTF-8"); 069 BufferedReader bufferedReader = new BufferedReader(reader); 070 try { 071 root = parseFile(bufferedReader, dir + "/", filePath, options); 072 } finally { 073 bufferedReader.close(); 074 reader.close(); 075 stream.close(); 076 } 077 } 078 options.putAll(oldOptions); 079 oldOptions = options; 080 options = new LinkedHashMap<String, String>(); 081 dir = new File(dir).getParent(); 082 } 083 } catch (IOException e) { 084 throw new EditorConfigException(null, e); 085 } 086 087 preprocessOptions(oldOptions); 088 089 final List<OutPair> result = new ArrayList<OutPair>(); 090 for (Map.Entry<String, String> keyValue : oldOptions.entrySet()) { 091 result.add(new OutPair(keyValue.getKey(), keyValue.getValue())); 092 } 093 return result; 094 } 095 096 private void checkAssertions() throws VersionException { 097 if (compareVersions(version, VERSION) > 0) { 098 throw new VersionException("Required version is greater than the current version."); 099 } 100 } 101 102 private static int compareVersions(String version1, String version2) { 103 String[] version1Components = version1.split("(\\.|-)"); 104 String[] version2Components = version2.split("(\\.|-)"); 105 for (int i = 0; i < 3; i++) { 106 String version1Component = version1Components[i]; 107 String version2Component = version2Components[i]; 108 int v1 = -1; 109 int v2 = -1; 110 try { 111 v1 = Integer.parseInt(version1Component); 112 } catch (NumberFormatException ignored) {} 113 try { 114 v2 = Integer.parseInt(version2Component); 115 } catch (NumberFormatException ignored) {} 116 if (v1 != v2) return v1 - v2; 117 } 118 return 0; 119 } 120 121 private void preprocessOptions(Map<String, String> options) { 122 // Lowercase option value for certain options 123 for (String key : new String[]{"end_of_line", "indent_style", "indent_size", "insert_final_newline", 124 "trim_trailing_whitespace", "charset"}) { 125 String value = options.get(key); 126 if (value != null) { 127 options.put(key, value.toLowerCase(Locale.US)); 128 } 129 } 130 131 // Set indent_size to "tab" if indent_size is unspecified and 132 // indent_style is set to "tab". 133 if ("tab".equals(options.get("indent_style")) && !options.containsKey("indent_size") && 134 compareVersions(version, "0.10.0") >= 0) { 135 options.put("indent_size", "tab"); 136 } 137 138 // Set tab_width to indent_size if indent_size is specified and 139 // tab_width is unspecified 140 String indent_size = options.get("indent_size"); 141 if (indent_size != null && !"tab".equals(indent_size) && !options.containsKey("tab_width")) { 142 options.put("tab_width", indent_size); 143 } 144 145 // Set indent_size to tab_width if indent_size is "tab" 146 String tab_width = options.get("tab_width"); 147 if ("tab".equals(indent_size) && tab_width != null) { 148 options.put("indent_size", tab_width); 149 } 150 } 151 152 private static boolean parseFile(BufferedReader bufferedReader, String dirName, String filePath, Map<String, String> result) throws IOException, EditorConfigException { 153 final StringBuilder malformedLines = new StringBuilder(); 154 boolean root = false; 155 boolean inSection = false; 156 boolean matchingSection = false; 157 while (bufferedReader.ready()) { 158 String line = bufferedReader.readLine().trim(); 159 160 if (line.startsWith("\ufeff")) { 161 line = line.substring(1); 162 } 163 164 // comment or blank line? 165 if (line.isEmpty() || line.startsWith("#") || line.startsWith(";")) continue; 166 167 Matcher matcher = SECTION_PATTERN.matcher(line); 168 if (matcher.matches()) { 169 inSection = true; 170 matchingSection = filenameMatches(dirName, matcher.group(HEADER), filePath); 171 continue; 172 } 173 matcher = OPTION_PATTERN.matcher(line); 174 if (matcher.matches()) { 175 String key = matcher.group(OPTION).trim().toLowerCase(Locale.US); 176 String value = matcher.group(VAL); 177 value = value.equals("\"\"") ? "" : value; 178 if (!inSection && "root".equals(key)) { 179 root = true; 180 } else if (matchingSection) { 181 int commentPos = value.indexOf(" ;"); 182 commentPos = commentPos < 0 ? value.indexOf(" #") : commentPos; 183 value = commentPos >= 0 ? value.substring(0, commentPos) : value; 184 result.put(key, value); 185 } 186 continue; 187 } 188 malformedLines.append(line).append("\n"); 189 } 190 if (malformedLines.length() > 0) { 191 throw new ParsingException(malformedLines.toString(), null); 192 } 193 return root; 194 } 195 196 private static boolean filenameMatches(String configDirname, String pattern, String filePath) { 197 pattern = pattern.replace(File.separatorChar, '/'); 198 pattern = pattern.replaceAll("\\\\#", "#"); 199 pattern = pattern.replaceAll("\\\\;", ";"); 200 int separator = pattern.indexOf("/"); 201 if (separator >= 0) { 202 pattern = configDirname + (separator == 0 ? pattern.substring(1) : pattern); 203 } else { 204 pattern = "**/" + pattern; 205 } 206 return Pattern.compile(convertGlobToRegEx(pattern)).matcher(filePath).matches(); 207 } 208 209 private static String convertGlobToRegEx(String pattern) { 210 int length = pattern.length(); 211 StringBuilder result = new StringBuilder(length); 212 int i = 0; 213 boolean escaped = false; 214 while (i < length) { 215 char current = pattern.charAt(i); 216 i++; 217 if ('*' == current) { 218 if (i < length && pattern.charAt(i) == '*') { 219 result.append(".*"); 220 i++; 221 } else { 222 result.append("[^/]*"); 223 } 224 } else if ('?' == current) { 225 result.append("."); 226 } else if ('[' == current) { 227 int j = i; 228 if (j < length && pattern.charAt(j) == '!') { 229 j++; 230 } 231 if (j < length && pattern.charAt(j) == ']') { 232 j++; 233 } 234 while (j < length && (pattern.charAt(j) != ']' || escaped)) { 235 escaped = pattern.charAt(j) == '\\' && !escaped; 236 j++; 237 } 238 if (j >= length) { 239 result.append("\\["); 240 } else { 241 String charClass = pattern.substring(i, j); 242 i = j + 1; 243 if (charClass.charAt(0) == '!') { 244 charClass = '^' + charClass; 245 } else if (charClass.charAt(0) == '^') { 246 charClass = "\\" + charClass; 247 } 248 result.append('[').append(charClass).append("]"); 249 } 250 } else if ('{' == current) { 251 int j = i; 252 final List<String> groups = new ArrayList<String>(); 253 while (j < length && pattern.charAt(j) != '}') { 254 int k = j; 255 while (k < length && (",}".indexOf(pattern.charAt(k)) < 0 || escaped)) { 256 escaped = pattern.charAt(k) == '\\' && !escaped; 257 k++; 258 } 259 String group = pattern.substring(j, k); 260 for (char c : new char[] {',', '}', '\\'}){ 261 group = group.replace("\\" + c, String.valueOf(c)); 262 } 263 groups.add(group); 264 j = k; 265 if (j < length && pattern.charAt(j) == ',') { 266 j++; 267 if (j < length && pattern.charAt(j) == '}') { 268 groups.add(""); 269 } 270 } 271 } 272 if (j >= length || groups.size() < 2) { 273 result.append("\\{"); 274 } else { 275 result.append("("); 276 for (int groupNumber = 0; groupNumber < groups.size(); groupNumber++) { 277 String group = groups.get(groupNumber); 278 if (groupNumber > 0) result.append("|"); 279 result.append(escapeToRegex(group)); 280 } 281 result.append(")"); 282 i = j + 1; 283 } 284 } else { 285 result.append(escapeToRegex(String.valueOf(current))); 286 } 287 } 288 289 return result.toString(); 290 } 291 292 private static String escapeToRegex(String group) { 293 final StringBuilder builder = new StringBuilder(group.length()); 294 for (char c : group.toCharArray()) { 295 if (c == ' ' || Character.isLetter(c) || Character.isDigit(c) || c == '_') { 296 builder.append(c); 297 } else if (c == '\n') { 298 builder.append("\\n"); 299 } else { 300 builder.append("\\").append(c); 301 } 302 } 303 return builder.toString(); 304 } 305 306 /** 307 * String-String pair to store the parsing result. 308 */ 309 public static class OutPair { 310 private final String key; 311 private final String val; 312 313 public OutPair(String key, String val) { 314 this.key = key; 315 this.val = val; 316 } 317 318 public String getKey(){ 319 return key; 320 } 321 322 public String getVal() { 323 return val; 324 } 325 } 326}