Skip to content

Commit 4ba41a6

Browse files
committed
feat: add native Java formatting and regex replacements
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
1 parent cf09d97 commit 4ba41a6

20 files changed

Lines changed: 1868 additions & 72 deletions

File tree

docs/linters.md

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -202,15 +202,35 @@ Lint Go code; uses --new-from-rev to scope analysis to changed code
202202

203203
### [`google-java-format`](https://github.com/google/google-java-format)
204204

205-
| | |
206-
| -------- | --------------------- |
207-
| Fix | yes |
208-
| Binary | `google-java-format` |
209-
| Scope | [files](#scope-files) |
210-
| Patterns | `*.java` |
205+
| | |
206+
| -------- | ----------------------- |
207+
| Fix | yes |
208+
| Binary | `google-java-format` |
209+
| Scope | [native](#scope-native) |
210+
| Patterns | `*.java` |
211211

212212
Format Java code
213213

214+
Format Java with google-java-format. Configure upstream options in `flint.toml`:
215+
216+
```toml
217+
[checks.google-java-format]
218+
skip_reflowing_long_strings = true
219+
skip_sorting_imports = true
220+
skip_removing_unused_imports = true
221+
skip_javadoc_formatting = true
222+
aosp = false
223+
```
224+
225+
`skip_reflowing_long_strings` defaults to `true` in Flint to match the
226+
current Spotless behavior. Set it to `false` to use google-java-format's
227+
upstream default. The other options default to `false`.
228+
229+
`off_on_markers` accepts `{ off = "...", on = "..." }` pairs for
230+
formatter-off regions that google-java-format does not understand.
231+
`patterns` and `exclude` control file ownership. Exclude patterns use
232+
the same glob syntax as `[settings].exclude`.
233+
214234
### [`hadolint`](https://github.com/hadolint/hadolint)
215235

216236
| | |
@@ -250,12 +270,14 @@ Disabled by default. Configure in `flint.toml`:
250270
```toml
251271
[checks.license-header]
252272
text = "SPDX-License-Identifier: Apache-2.0"
253-
patterns = ["*.java", "*.kt"]
273+
patterns = ["*.java", "*.kt", "*.scala", "*.groovy"]
274+
exclude = ["package-info.java"]
254275
lines_to_check = 5
255276
```
256277

257278
- `text` — required header text to find near the top of each file
258279
- `patterns` — glob patterns selecting which files to check
280+
- `exclude` — glob patterns excluded after `patterns`
259281
- `lines_to_check` — how many leading lines to search; defaults to `5`
260282

261283
`text` may be multi-line. Flint joins the first `lines_to_check` lines with
@@ -301,6 +323,57 @@ config = ".github/config/lychee.toml"
301323
check_all_local = true
302324
```
303325

326+
### `regex-replace`
327+
328+
| | |
329+
| ------ | ------------------------------------------ |
330+
| Fix | yes |
331+
| Binary | (built-in) |
332+
| Scope | [native](#scope-native) |
333+
| Config | via `[checks.regex-replace]` in flint.toml |
334+
335+
Apply configured regular-expression replacements to source files
336+
337+
Applies ordered regular-expression replacement rule sets to source files.
338+
Each set has its own file scope, replacement default, line filters, ignored regions,
339+
import insertion policy, and rules.
340+
341+
```toml
342+
[checks.regex-replace]
343+
patterns = ["*.txt"]
344+
exclude = ["generated/**"]
345+
346+
[[checks.regex-replace.sets]]
347+
name = "example"
348+
replacement = '$1'
349+
add_lines_before_pattern = '^use '
350+
351+
[[checks.regex-replace.sets.rules]]
352+
pattern = '\bfoo\.(bar)\b'
353+
add_lines = ['use foo.$1;']
354+
```
355+
356+
Top-level `patterns` and `exclude` select files for the check; a set's optional
357+
`patterns` and `exclude` further restrict that set. Rules inherit the set's
358+
`replacement`; a rule can override it. If neither is configured, replacement
359+
defaults to `$0` (the original match). `pattern` is matched line by line.
360+
Capture groups can be used in replacements and `add_lines`.
361+
362+
Use `derived_rules` when a rule must be generated from named captures in a
363+
`source_pattern`; `{name}` placeholders are substituted into the generated rule.
364+
`content_pattern`, `content_exclude_pattern`, `line_exclude_pattern`, and
365+
`file_pattern` are rule-level filters. `skip_line_pattern` and `ignore_regions`
366+
are set-level filters; an ignored region is defined by configurable
367+
`start_pattern` and `end_pattern` regexes matched against whole lines; the
368+
markers and all lines between them are ignored, and the pair must be balanced.
369+
`add_lines_before_pattern` inserts
370+
unique added lines before the first matching line;
371+
`add_lines_fallback_after_pattern` inserts them after its first match when
372+
there is no before-pattern match. Derived rules also support
373+
`source_exclude_pattern`. Set and check file exclusions use the same glob
374+
syntax as `[settings].exclude`. See [the regex-replace guide](linters/regex-replace.md)
375+
for a complete static-import example.
376+
304377
### [`renovate-deps`](https://docs.renovatebot.com/)
305378

306379
| | |

docs/linters/regex-replace.md

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# `regex-replace`
2+
3+
`regex-replace` is Flint's built-in, line-oriented rewrite engine. It applies
4+
regular-expression rules to selected files and can add text when a rule
5+
matches. It does not parse a programming language or contain Java-specific
6+
logic; the language convention is expressed by the configured regular
7+
expressions.
8+
9+
This makes it useful for repository-specific mechanical changes that do not
10+
belong in a general-purpose formatter.
11+
12+
## Example: qualified Java references to static imports
13+
14+
One example is replacing frequently used qualified Java references with static
15+
imports. Given:
16+
17+
```java
18+
import java.util.Objects;
19+
20+
class Example {
21+
void check(Object value) {
22+
Objects.requireNonNull(value);
23+
}
24+
}
25+
```
26+
27+
the desired result is:
28+
29+
```java
30+
import static java.util.Objects.requireNonNull;
31+
import java.util.Objects;
32+
33+
class Example {
34+
void check(Object value) {
35+
requireNonNull(value);
36+
}
37+
}
38+
```
39+
40+
The rule has two effects:
41+
42+
1. It rewrites `Objects.requireNonNull` to `requireNonNull`.
43+
2. It adds the corresponding import once, before the first import or after
44+
the package declaration when the file has no imports.
45+
46+
Here is a realistic configuration based on the static-import migration this
47+
check was designed to support:
48+
49+
```toml
50+
[checks.regex-replace]
51+
patterns = ["*.java"]
52+
exclude = ["generated/**", "build/**"]
53+
54+
[[checks.regex-replace.sets]]
55+
name = "static-imports"
56+
57+
# Rules inherit this replacement unless they specify one themselves.
58+
# $1 is the first capture group in each direct rule.
59+
replacement = '$1'
60+
skip_line_pattern = '^\s*import '
61+
add_lines_before_pattern = '^\s*import '
62+
add_lines_fallback_after_pattern = '^\s*package '
63+
64+
# Do not rewrite examples or references inside block comments.
65+
ignore_regions = [
66+
{ start_pattern = '^\s*/\*', end_pattern = '\*/' },
67+
]
68+
69+
[[checks.regex-replace.sets.rules]]
70+
pattern = '\bObjects\.(requireNonNull)\b'
71+
add_lines = ['import static java.util.Objects.$1;']
72+
73+
[[checks.regex-replace.sets.rules]]
74+
pattern = '\bElementMatchers\.([a-z][a-zA-Z0-9]*)\b'
75+
add_lines = ['import static net.bytebuddy.matcher.ElementMatchers.$1;']
76+
77+
[[checks.regex-replace.sets.rules]]
78+
pattern = '\bMockito\.(mock|mockStatic|spy|when|verify|never|times)\b'
79+
add_lines = ['import static org.mockito.Mockito.$1;']
80+
81+
[[checks.regex-replace.sets.rules]]
82+
pattern = '(^|[^.])\bLevel\.([A-Z][A-Z_0-9]*)\b'
83+
replacement = '$1$2'
84+
add_lines = ['import static java.util.logging.Level.$2;']
85+
content_pattern = '(?m)^\s*import java\.util\.logging\.Level;$'
86+
87+
[[checks.regex-replace.sets.rules]]
88+
pattern = '\bAttributeKey\.(stringKey|longKey|booleanKey|doubleKey)\b'
89+
add_lines = ['import static io.opentelemetry.api.common.AttributeKey.$1;']
90+
line_exclude_pattern = '= AttributeKey\.'
91+
file_pattern = 'Test\.java$'
92+
93+
# Generate rules from existing imports. For example, an import of
94+
# io.opentelemetry.semconv.http.HttpAttributes lets the same rule rewrite
95+
# HttpAttributes.HTTP_REQUEST_METHOD and add the matching static import.
96+
[[checks.regex-replace.sets.derived_rules]]
97+
source_pattern = '^import (?P<package>io\.opentelemetry\.semconv(?:\.[a-z][a-z.]*)?\.)(?P<class>[A-Z][a-zA-Z0-9]+);$'
98+
pattern = '\b{class}\.([A-Z][A-Z_0-9]*)\b'
99+
add_lines = ['import static {package}$1;']
100+
source_exclude_pattern = 'SchemaUrls'
101+
```
102+
103+
Run it like any other fixable Flint check:
104+
105+
```bash
106+
flint run regex-replace
107+
flint run --fix regex-replace
108+
```
109+
110+
## How the configuration is organized
111+
112+
### File selection
113+
114+
The check-level `patterns` and `exclude` select the files Flint gives to the
115+
linter. Each rule set can add its own `patterns` and `exclude`, using the same
116+
glob syntax as `[settings].exclude`:
117+
118+
```toml
119+
[checks.regex-replace]
120+
patterns = ["*.java", "*.kt"]
121+
exclude = ["generated/**"]
122+
123+
[[checks.regex-replace.sets]]
124+
name = "java-only"
125+
patterns = ["*.java"]
126+
exclude = ["examples/**"]
127+
```
128+
129+
This lets one `regex-replace` check contain independent rule sets for
130+
different file types or directory scopes.
131+
132+
### Rule sets and defaults
133+
134+
Sets are evaluated in order. A set groups rules that share policy and defaults:
135+
136+
- `replacement` is inherited by direct and derived rules.
137+
- A rule-level `replacement` overrides the set default.
138+
- If neither is configured, the replacement is `$0`, preserving the match.
139+
- `add_lines_before_pattern` and
140+
`add_lines_fallback_after_pattern` control where unique added lines go.
141+
- `skip_line_pattern` skips entire lines for every rule in the set.
142+
- `ignore_regions` skips balanced regions for every rule in the set.
143+
144+
Added lines are deduplicated against the file, so rerunning the fixer does not
145+
keep adding the same import.
146+
147+
### Rule filters
148+
149+
Direct rules support additional filters:
150+
151+
- `content_pattern` — only apply when the whole file contains a match.
152+
- `content_exclude_pattern` — skip the rule when the whole file contains a
153+
match.
154+
- `line_exclude_pattern` — skip a line when its nearby context matches.
155+
- `file_pattern` — restrict the rule using the file name.
156+
157+
Derived rules use `source_pattern` to find source lines and named captures such
158+
as `{package}` and `{class}` to generate a rule. Their
159+
`source_exclude_pattern` prevents selected source lines from generating rules.
160+
Capture groups from the generated rule remain available as `$1`, `$2`, and so
161+
on in `replacement` and `add_lines`.
162+
163+
### Ignored regions
164+
165+
`ignore_regions` is line-based and generic. Each `start_pattern` and
166+
`end_pattern` is a regular expression matched against a complete source line;
167+
the marker lines and every line between them are skipped. The markers must be
168+
balanced. This is useful for block comments, generated snippets, or repository
169+
conventions that should not be rewritten.
170+
171+
It is deliberately separate from formatter-specific formatter-off handling:
172+
`regex-replace` skips those lines, while a formatter integration may instead
173+
format a temporary copy and restore the protected contents afterward.
174+
175+
## Limitations
176+
177+
`regex-replace` is intentionally not a parser or import sorter. It cannot
178+
prove that a replacement is syntactically valid, resolve overloaded methods,
179+
or determine whether a static import conflicts with another symbol. Keep the
180+
patterns narrow, use content and file filters where needed, and review the
181+
result of a new rule with `flint run --fix` before enabling it broadly.

0 commit comments

Comments
 (0)