Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inception (languages within languages within...) #696

Merged
merged 12 commits into from
Sep 21, 2020
Merged
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ This document is intended for Spotless developers.
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).

## [Unreleased]
### Added
* `PipeStepPair.Builder` now has a method `.buildStepWhichAppliesSubSteps(Path rootPath, Collection<FormatterStep> steps)`, which returns a single `FormatterStep` that applies the given steps within the regex defined earlier in the builder. Used for formatting inception (implements [#412](https://github.com/diffplug/spotless/issues/412)).

## [2.6.2] - 2020-09-18
### Fixed
Expand Down
113 changes: 83 additions & 30 deletions lib/src/main/java/com/diffplug/spotless/generic/PipeStepPair.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,21 @@
*/
package com.diffplug.spotless.generic;

import java.io.File;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.diffplug.spotless.Formatter;
import com.diffplug.spotless.FormatterFunc;
import com.diffplug.spotless.FormatterStep;
import com.diffplug.spotless.LineEnding;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

Expand Down Expand Up @@ -63,13 +71,21 @@ public Builder regex(String regex) {

/** Defines the pipe via regex. Must have *exactly one* capturing group. */
public Builder regex(Pattern regex) {
this.regex = regex;
this.regex = Objects.requireNonNull(regex);
return this;
}

/** Returns a pair of steps which captures in the first part, then returns in the second. */
public PipeStepPair buildPair() {
return new PipeStepPair(name, regex);
}

/** Returns a single step which will apply the given steps only within the blocks selected by the regex / openClose pair. */
public FormatterStep buildStepWhichAppliesSubSteps(Path rootPath, Collection<? extends FormatterStep> steps) {
return FormatterStep.createLazy(name,
() -> new StateApplyToBlock(regex, steps),
state -> FormatterFunc.Closeable.of(state.buildFormatter(rootPath), state::format));
}
}

final FormatterStep in, out;
Expand All @@ -89,19 +105,52 @@ public FormatterStep out() {
return out;
}

@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
static class StateApplyToBlock extends StateIn implements Serializable {
private static final long serialVersionUID = -844178006407733370L;

final List<FormatterStep> steps;
final transient StringBuilder builder = new StringBuilder();

StateApplyToBlock(Pattern regex, Collection<? extends FormatterStep> steps) {
super(regex);
this.steps = new ArrayList<>(steps);
}

Formatter buildFormatter(Path rootDir) {
return Formatter.builder()
.encoding(StandardCharsets.UTF_8) // can be any UTF, doesn't matter
.lineEndingsPolicy(LineEnding.UNIX.createPolicy()) // just internal, won't conflict with user
.steps(steps)
.rootDir(rootDir)
.build();
}

private String format(Formatter formatter, String unix, File file) throws Exception {
groups.clear();
Matcher matcher = regex.matcher(unix);
while (matcher.find()) {
// apply the formatter to each group
groups.add(formatter.compute(matcher.group(1), file));
}
// and then assemble the result right away
return stateOutCompute(this, builder, unix);
}
}

@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
static class StateIn implements Serializable {
private static final long serialVersionUID = -844178006407733370L;

final Pattern regex;

public StateIn(Pattern regex) {
this.regex = regex;
this.regex = Objects.requireNonNull(regex);
}

final transient ArrayList<String> groups = new ArrayList<>();

private String format(String unix) {
private String format(String unix) throws Exception {
groups.clear();
Matcher matcher = regex.matcher(unix);
while (matcher.find()) {
Expand All @@ -118,40 +167,44 @@ static class StateOut implements Serializable {
final StateIn in;

StateOut(StateIn in) {
this.in = in;
this.in = Objects.requireNonNull(in);
}

final transient StringBuilder builder = new StringBuilder();

private String format(String unix) {
if (in.groups.isEmpty()) {
return unix;
}
builder.setLength(0);
Matcher matcher = in.regex.matcher(unix);
int lastEnd = 0;
int groupIdx = 0;
while (matcher.find()) {
builder.append(unix, lastEnd, matcher.start(1));
builder.append(in.groups.get(groupIdx));
lastEnd = matcher.end(1);
++groupIdx;
}
if (groupIdx == in.groups.size()) {
builder.append(unix, lastEnd, unix.length());
return builder.toString();
return stateOutCompute(in, builder, unix);
}
}

private static String stateOutCompute(StateIn in, StringBuilder builder, String unix) {
if (in.groups.isEmpty()) {
return unix;
}
builder.setLength(0);
Matcher matcher = in.regex.matcher(unix);
int lastEnd = 0;
int groupIdx = 0;
while (matcher.find()) {
builder.append(unix, lastEnd, matcher.start(1));
builder.append(in.groups.get(groupIdx));
lastEnd = matcher.end(1);
++groupIdx;
}
if (groupIdx == in.groups.size()) {
builder.append(unix, lastEnd, unix.length());
return builder.toString();
} else {
// throw an error with either the full regex, or the nicer open/close pair
Matcher openClose = Pattern.compile("\\\\Q([\\s\\S]*?)\\\\E" + "\\Q([\\s\\S]*?)\\E" + "\\\\Q([\\s\\S]*?)\\\\E")
.matcher(in.regex.pattern());
String pattern;
if (openClose.matches()) {
pattern = openClose.group(1) + " " + openClose.group(2);
} else {
// throw an error with either the full regex, or the nicer open/close pair
Matcher openClose = Pattern.compile("\\\\Q([\\s\\S]*?)\\\\E" + "\\Q([\\s\\S]*?)\\E" + "\\\\Q([\\s\\S]*?)\\\\E")
.matcher(in.regex.pattern());
String pattern;
if (openClose.matches()) {
pattern = openClose.group(1) + " " + openClose.group(2);
} else {
pattern = in.regex.pattern();
}
throw new Error("An intermediate step removed a match of " + pattern);
pattern = in.regex.pattern();
}
throw new Error("An intermediate step removed a match of " + pattern);
}
}
}
2 changes: 2 additions & 0 deletions plugin-gradle/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).

## [Unreleased]
### Added
* [`withinBlocks` allows you to apply rules only to specific sections of files](README.md#inception-languages-within-languages-within), for example formatting javascript within html, code examples within markdown, and things like that (implements [#412](https://github.com/diffplug/spotless/issues/412) - formatting inception).

## [5.5.2] - 2020-09-18
### Fixed
Expand Down
20 changes: 20 additions & 0 deletions plugin-gradle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui
- [Line endings and encodings (invisible stuff)](#line-endings-and-encodings-invisible-stuff)
- [Custom steps](#custom-steps)
- [Multiple (or custom) language-specific blocks](#multiple-or-custom-language-specific-blocks)
- [Inception (languages within languages within...)](#inception-languages-within-languages-within)
- [Disabling warnings and error messages](#disabling-warnings-and-error-messages)
- [How do I preview what `spotlessApply` will do?](#how-do-i-preview-what-spotlessapply-will-do)
- [Example configurations (from real-world projects)](#example-configurations-from-real-world-projects)
Expand Down Expand Up @@ -774,6 +775,25 @@ spotless {

If you'd like to create a one-off Spotless task outside of the `check`/`apply` framework, see [`FormatExtension.createIndependentApplyTask`](https://javadoc.io/static/com.diffplug.spotless/spotless-plugin-gradle/5.5.2/com/diffplug/gradle/spotless/FormatExtension.html#createIndependentApplyTask-java.lang.String-).

## Inception (languages within languages within...)

In very rare cases, you might want to format e.g. javascript which is written inside JSP templates, or maybe java within a markdown file, or something wacky like that. You can specify hunks within a file using either open/close tags or a regex with a single capturing group, and then specify rules within it, like so. See [javadoc TODO](https://javadoc.io/static/com.diffplug.spotless/spotless-plugin-gradle/5.5.1/com/diffplug/gradle/spotless/FormatExtension.html#target-java.lang.Object...-) for more details.

```gradle
import com.diffplug.gradle.spotless.JavaExtension

spotless {
format 'templates', {
target 'src/templates/**/*.foo.html'
prettier().config(['parser': 'html'])
withinBlocks 'javascript block', '<script>', '</script>', {
prettier().config(['parser': 'javascript'])
}
withinBlocksRegex 'single-line @(java-expresion)', '@\\((.*?)\\)', JavaExtension, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not clear on what the regex @\\((.*?)\\) matches. Should we add a comment or replace it with the example in the Javadoc of FormatExtension.withinBlocks?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In some java-html templating systems (I'm thinking of rocker and play framework 2), you can pass java variables to templates, and render any arbitrary expression with @(someExpression), e.g. @(i+1). I'm happy for this regex to get changed to anything else, I personally always find them difficult to read. IMO, the intention is just to show "pass a string and it will be used as a regex".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh okay, that makes a lot of sense! Goes to show that I'm not familiar with that class of templating systems. 😛

My opinion is that the example in the doc for FormatExtension.withinBlocks is easier to understand, since it doesn't rely on an understanding of a certain type of templating system, which I think it makes the intention clearer. But I'm happy to defer to you on this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you, but not enough to dig up the javadoc, make the change, and push it. If you want to do it, by all means :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise I'm not fussed enough to want to make the change myself, so let's leave it, it's good enough. :)

googleJavaFormat()
}
```

<a name="enforceCheck"></a>

## Disabling warnings and error messages
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@

import java.util.Objects;

import javax.inject.Inject;

import com.diffplug.spotless.FormatterStep;
import com.diffplug.spotless.antlr4.Antlr4Defaults;
import com.diffplug.spotless.antlr4.Antlr4FormatterStep;

public class Antlr4Extension extends FormatExtension implements HasBuiltinDelimiterForLicense {
static final String NAME = "antlr4";

@Inject
public Antlr4Extension(SpotlessExtension rootExtension) {
super(rootExtension);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import static com.diffplug.gradle.spotless.PluginGradlePreconditions.requireElementsNonNull;

import javax.inject.Inject;

import org.gradle.api.Project;

import com.diffplug.spotless.cpp.CppDefaults;
Expand All @@ -26,6 +28,7 @@
public class CppExtension extends FormatExtension implements HasBuiltinDelimiterForLicense {
static final String NAME = "cpp";

@Inject
public CppExtension(SpotlessExtension spotless) {
super(spotless);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.util.stream.Stream;

import javax.annotation.Nullable;
import javax.inject.Inject;

import org.gradle.api.Action;
import org.gradle.api.GradleException;
Expand Down Expand Up @@ -66,6 +67,7 @@ public class FormatExtension {
final SpotlessExtension spotless;
final List<Action<FormatExtension>> lazyActions = new ArrayList<>();

@Inject
public FormatExtension(SpotlessExtension spotless) {
this.spotless = Objects.requireNonNull(spotless);
}
Expand Down Expand Up @@ -624,6 +626,59 @@ public EclipseWtpConfig eclipseWtp(EclipseWtpFormatterStep type, String version)
return new EclipseWtpConfig(type, version);
}

/**
* ```gradle
* spotless {
* format 'examples', {
* target 'src/**\/*.md'
* withinBlocks 'javascript examples', '\n```javascript\n', '\n```\n`, {
* prettier().config(['parser': 'javascript'])
* }
* ...
* ```
*/
public void withinBlocks(String name, String open, String close, Action<FormatExtension> configure) {
withinBlocks(name, open, close, FormatExtension.class, configure);
}

/**
* Same as {@link #withinBlocks(String, String, String, Action)}, except you can specify
* any language-specific subclass of {@link FormatExtension} to get language-specific steps.
*
* ```gradle
* spotless {
* format 'examples', {
* target 'src/**\/*.md'
* withinBlocks 'java examples', '\n```java\n', '\n```\n`, com.diffplug.gradle.spotless.JavaExtension, {
* googleJavaFormat()
* }
* ...
* ```
*/
public <T extends FormatExtension> void withinBlocks(String name, String open, String close, Class<T> clazz, Action<T> configure) {
withinBlocksHelper(PipeStepPair.named(name).openClose(open, close), clazz, configure);
}

/** Same as {@link #withinBlocks(String, String, String, Action)}, except instead of an open/close pair, you specify a regex with exactly one capturing group. */
public void withinBlocksRegex(String name, String regex, Action<FormatExtension> configure) {
withinBlocksRegex(name, regex, FormatExtension.class, configure);
}

/** Same as {@link #withinBlocksRegex(String, String, Action)}, except you can specify any language-specific subclass of {@link FormatExtension} to get language-specific steps. */
public <T extends FormatExtension> void withinBlocksRegex(String name, String regex, Class<T> clazz, Action<T> configure) {
withinBlocksHelper(PipeStepPair.named(name).regex(regex), clazz, configure);
}

private <T extends FormatExtension> void withinBlocksHelper(PipeStepPair.Builder builder, Class<T> clazz, Action<T> configure) {
// create the sub-extension
T formatExtension = spotless.instantiateFormatExtension(clazz);
// configure it
configure.execute(formatExtension);
// create a step which applies all of those steps as sub-steps
FormatterStep step = builder.buildStepWhichAppliesSubSteps(spotless.project.getRootDir().toPath(), formatExtension.steps);
addStep(step);
}

/**
* Given a regex with *exactly one capturing group*, disables formatting
* inside that captured group.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import java.util.Map;
import java.util.Objects;

import javax.inject.Inject;

import org.gradle.api.Action;

import com.diffplug.spotless.FormatterProperties;
Expand All @@ -33,6 +35,7 @@ public class FreshMarkExtension extends FormatExtension {

public final List<Action<Map<String, Object>>> propertyActions = new ArrayList<>();

@Inject
public FreshMarkExtension(SpotlessExtension spotless) {
super(spotless);
addStep(FreshMarkStep.create(() -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

import java.util.Objects;

import javax.inject.Inject;

import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.file.FileCollection;
Expand All @@ -36,6 +38,7 @@
public class GroovyExtension extends FormatExtension implements HasBuiltinDelimiterForLicense {
static final String NAME = "groovy";

@Inject
public GroovyExtension(SpotlessExtension spotless) {
super(spotless);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@

import java.util.Objects;

import javax.inject.Inject;

import com.diffplug.spotless.extra.groovy.GrEclipseFormatterStep;
import com.diffplug.spotless.java.ImportOrderStep;

public class GroovyGradleExtension extends FormatExtension {
private static final String GRADLE_FILE_EXTENSION = "*.gradle";
static final String NAME = "groovyGradle";

@Inject
public GroovyGradleExtension(SpotlessExtension spotless) {
super(spotless);
}
Expand Down
Loading