Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright 2025 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 org.openrewrite.config;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

/**
* Parses a single {@code .editorconfig} file into a structured representation.
*/
public class EditorConfigParser {

public static class EditorConfigFile {
private final boolean root;
private final List<Section> sections;

public EditorConfigFile(boolean root, List<Section> sections) {
this.root = root;
this.sections = sections;
}

public boolean isRoot() {
return root;
}

public List<Section> getSections() {
return sections;
}
}

public static class Section {
private final String pattern;
private final Map<String, String> properties;

public Section(String pattern, Map<String, String> properties) {
this.pattern = pattern;
this.properties = properties;
}

public String getPattern() {
return pattern;
}

public Map<String, String> getProperties() {
return properties;
}
}

public EditorConfigFile parse(Path path) throws IOException {
List<String> lines = Files.readAllLines(path);
return parse(lines);
}

public EditorConfigFile parse(List<String> lines) {
boolean root = false;
List<Section> sections = new ArrayList<>();
String currentPattern = null;
Map<String, String> currentProperties = null;

for (String rawLine : lines) {
String line = rawLine.trim();

// Skip empty lines and comments
if (line.isEmpty() || line.charAt(0) == '#' || line.charAt(0) == ';') {
continue;
}

// Section header
if (line.startsWith("[") && line.endsWith("]")) {
if (currentPattern != null && currentProperties != null) {
sections.add(new Section(currentPattern, currentProperties));
}
currentPattern = line.substring(1, line.length() - 1).trim();
currentProperties = new LinkedHashMap<>();
continue;
}

// Key-value pair
int eqIdx = line.indexOf('=');
if (eqIdx < 0) {
continue;
}
String key = line.substring(0, eqIdx).trim().toLowerCase(Locale.ENGLISH);
String value = line.substring(eqIdx + 1).trim().toLowerCase(Locale.ENGLISH);

if (currentPattern == null) {
// Preamble (before any section)
if ("root".equals(key) && "true".equals(value)) {
root = true;
}
} else {
currentProperties.put(key, value);
}
}

// Flush last section
if (currentPattern != null && currentProperties != null) {
sections.add(new Section(currentPattern, currentProperties));
}

return new EditorConfigFile(root, sections);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright 2025 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 org.openrewrite.config;

import org.openrewrite.PathUtils;
import org.openrewrite.internal.lang.Nullable;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

/**
* Resolves effective {@code .editorconfig} properties for a given file by walking
* directories upward from the file's location to the project root, collecting and
* merging matching sections. Child directories override parent directories.
*/
public class EditorConfigResolver {
private static final String EDITOR_CONFIG_FILE = ".editorconfig";

private final @Nullable Path projectRoot;
private final EditorConfigParser parser = new EditorConfigParser();
private final Map<Path, EditorConfigParser.EditorConfigFile> parsedFileCache = new HashMap<>();
private final Map<Path, List<EditorConfigParser.EditorConfigFile>> configsPerDirectory = new HashMap<>();

public EditorConfigResolver(@Nullable Path projectRoot) {
this.projectRoot = projectRoot != null ? projectRoot.toAbsolutePath().normalize() : null;
}

/**
* Resolve the effective editorconfig properties for the given file path.
*
* @param filePath absolute path to the file
* @return merged properties map, or empty if no .editorconfig applies
*/
public Map<String, String> resolve(Path filePath) {
Path absPath = filePath.toAbsolutePath().normalize();
Path dir = absPath.getParent();
if (dir == null) {
return Collections.emptyMap();
}

String fileName = absPath.getFileName().toString();
List<EditorConfigParser.EditorConfigFile> configs = collectConfigs(dir);

// Merge matching sections
Map<String, String> merged = new LinkedHashMap<>();
for (EditorConfigParser.EditorConfigFile config : configs) {
for (EditorConfigParser.Section section : config.getSections()) {
if (PathUtils.matchesGlob(fileName, section.getPattern())) {
merged.putAll(section.getProperties());
}
}
}
return merged;
}

private List<EditorConfigParser.EditorConfigFile> collectConfigs(Path dir) {
List<EditorConfigParser.EditorConfigFile> cached = configsPerDirectory.get(dir);
if (cached != null) {
return cached;
}

List<EditorConfigParser.EditorConfigFile> configs = new ArrayList<>();
Path current = dir;
while (current != null) {
Path ecFile = current.resolve(EDITOR_CONFIG_FILE);
if (Files.isRegularFile(ecFile)) {
EditorConfigParser.EditorConfigFile parsed = parseFile(ecFile);
if (parsed != null) {
configs.add(parsed);
if (parsed.isRoot()) {
break;
}
}
}
if (projectRoot != null && current.equals(projectRoot)) {
break;
}
Path parent = current.getParent();
if (parent != null && parent.equals(current)) {
break;
}
current = parent;
}

// Reverse so parents come first, children override
Collections.reverse(configs);
configsPerDirectory.put(dir, configs);
return configs;
}

private @Nullable EditorConfigParser.EditorConfigFile parseFile(Path ecFile) {
return parsedFileCache.computeIfAbsent(ecFile, f -> {
try {
return parser.parse(f);
} catch (IOException e) {
return null;
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2025 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 org.openrewrite.config;

import org.jspecify.annotations.Nullable;
import org.openrewrite.style.GeneralFormatStyle;

import java.util.Map;

/**
* Static helpers for mapping universal {@code .editorconfig} properties to
* language-neutral style values. Language modules compose these into their
* own style objects.
*/
public final class EditorConfigStyles {
private EditorConfigStyles() {
}

/**
* @return {@code true} for tabs, {@code false} for spaces, {@code null} if unset
*/
public static @Nullable Boolean useTabCharacter(Map<String, String> props) {
String value = props.get("indent_style");
if ("tab".equals(value)) {
return true;
} else if ("space".equals(value)) {
return false;
}
return null;
}

/**
* @return the indent size, or {@code null} if unset. Handles the special case
* where {@code indent_size=tab} means use the {@code tab_width} value.
*/
public static @Nullable Integer indentSize(Map<String, String> props) {
String value = props.get("indent_size");
if (value == null) {
return null;
}
if ("tab".equals(value)) {
return tabSize(props);
}
return parsePositiveInt(value);
}

/**
* @return the tab size, or {@code null} if unset. Falls back to {@code indent_size}
* if {@code tab_width} is not explicitly set.
*/
public static @Nullable Integer tabSize(Map<String, String> props) {
String value = props.get("tab_width");
if (value != null) {
return parsePositiveInt(value);
}
// Per spec, tab_width defaults to indent_size when not set
String indentSizeValue = props.get("indent_size");
if (indentSizeValue != null && !"tab".equals(indentSizeValue)) {
return parsePositiveInt(indentSizeValue);
}
return null;
}

/**
* @return a {@link GeneralFormatStyle} based on {@code end_of_line}, or {@code null} if unset
*/
public static @Nullable GeneralFormatStyle generalFormatStyle(Map<String, String> props) {
String value = props.get("end_of_line");
if ("crlf".equals(value)) {
return new GeneralFormatStyle(true);
} else if ("lf".equals(value) || "cr".equals(value)) {
return new GeneralFormatStyle(false);
}
return null;
}

private static @Nullable Integer parsePositiveInt(String value) {
try {
int parsed = Integer.parseInt(value);
return parsed > 0 ? parsed : null;
} catch (NumberFormatException e) {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.jspecify.annotations.Nullable;
import org.openrewrite.DelegatingExecutionContext;
import org.openrewrite.ExecutionContext;
import org.openrewrite.config.EditorConfigResolver;

import java.nio.charset.Charset;

Expand All @@ -26,6 +27,8 @@ public class ParsingExecutionContextView extends DelegatingExecutionContext {

private static final String CHARSET = "org.openrewrite.parser.charset";

private static final String EDITOR_CONFIG_RESOLVER = "org.openrewrite.parser.editorConfigResolver";

public ParsingExecutionContextView(ExecutionContext delegate) {
super(delegate);
}
Expand Down Expand Up @@ -54,4 +57,13 @@ public ParsingExecutionContextView setCharset(@Nullable Charset charset) {
public @Nullable Charset getCharset() {
return getMessage(CHARSET);
}

public ParsingExecutionContextView setEditorConfigResolver(EditorConfigResolver resolver) {
putMessage(EDITOR_CONFIG_RESOLVER, resolver);
return this;
}

public @Nullable EditorConfigResolver getEditorConfigResolver() {
return getMessage(EDITOR_CONFIG_RESOLVER);
}
}
Loading