From 834c6c079d0373ebb2c520559384ca21414d1ec4 Mon Sep 17 00:00:00 2001 From: Claudia Rogoz Date: Fri, 26 Apr 2024 18:02:05 +0200 Subject: [PATCH] Move the plugin to the FormattingService & ImportOptimizer APIs (#1078) --- .gitignore | 3 +- .palantir/revapi.yml | 6 + build.gradle | 2 +- changelog/@unreleased/pr-1078.v2.yml | 5 + idea-plugin/build.gradle | 5 +- .../intellij/CodeStyleManagerDecorator.java | 233 ------------------ .../intellij/FormatterProvider.java | 11 +- .../javaformat/intellij/Notifications.java | 39 +++ .../intellij/PalantirCodeStyleManager.java | 222 ----------------- .../PalantirJavaFormatFormattingService.java | 157 ++++++++++++ .../PalantirJavaFormatImportOptimizer.java | 92 +++++++ .../src/main/resources/META-INF/plugin.xml | 12 +- ...lantirJavaFormatFormattingServiceTest.java | 152 ++++++++++++ ...PalantirJavaFormatImportOptimizerTest.java | 185 ++++++++++++++ .../BootstrappingFormatterService.java | 14 +- .../javaformat/java/FormatterService.java | 11 +- .../palantir/javaformat/java/Formatter.java | 13 + .../javaformat/java/FormatterServiceImpl.java | 5 + versions.props | 1 + 19 files changed, 697 insertions(+), 471 deletions(-) create mode 100644 changelog/@unreleased/pr-1078.v2.yml delete mode 100644 idea-plugin/src/main/java/com/palantir/javaformat/intellij/CodeStyleManagerDecorator.java create mode 100644 idea-plugin/src/main/java/com/palantir/javaformat/intellij/Notifications.java delete mode 100644 idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirCodeStyleManager.java create mode 100644 idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatFormattingService.java create mode 100644 idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatImportOptimizer.java create mode 100644 idea-plugin/src/test/java/com/palantir/javaformat/intellij/PalantirJavaFormatFormattingServiceTest.java create mode 100644 idea-plugin/src/test/java/com/palantir/javaformat/intellij/PalantirJavaFormatImportOptimizerTest.java diff --git a/.gitignore b/.gitignore index 707d4e8b8..912597cf3 100644 --- a/.gitignore +++ b/.gitignore @@ -56,4 +56,5 @@ generated/ # Blueprint theme __init__.pyc -node_modules/ \ No newline at end of file +node_modules/ +/.profileconfig.json diff --git a/.palantir/revapi.yml b/.palantir/revapi.yml index cdae5ef12..430942cd5 100644 --- a/.palantir/revapi.yml +++ b/.palantir/revapi.yml @@ -16,3 +16,9 @@ acceptedBreaks: old: "method void com.palantir.javaformat.gradle.ConfigureJavaFormatterXml::configureFormatOnSave(groovy.util.Node)" new: "method void com.palantir.javaformat.gradle.ConfigureJavaFormatterXml::configureFormatOnSave(groovy.util.Node)" justification: "Not public api, not split over multiple jars" + "2.43.0": + com.palantir.javaformat:palantir-java-format-spi: + - code: "java.method.addedToInterface" + new: "method java.lang.String com.palantir.javaformat.java.FormatterService::fixImports(java.lang.String)\ + \ throws com.palantir.javaformat.java.FormatterException" + justification: "new fixImports only method added to spi" diff --git a/build.gradle b/build.gradle index 52af4fb48..f2850472b 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ buildscript { } plugins { - id "org.jetbrains.intellij" version "1.3.0" apply false + id "org.jetbrains.intellij" version "1.17.3" apply false id 'org.jetbrains.gradle.plugin.idea-ext' version "1.1.1" } diff --git a/changelog/@unreleased/pr-1078.v2.yml b/changelog/@unreleased/pr-1078.v2.yml new file mode 100644 index 000000000..cc261ed69 --- /dev/null +++ b/changelog/@unreleased/pr-1078.v2.yml @@ -0,0 +1,5 @@ +type: fix +fix: + description: Fix palantir-java-format not running on IntelliJ >=2024.1 by moving to new APIs. + links: + - https://github.com/palantir/palantir-java-format/pull/1078 diff --git a/idea-plugin/build.gradle b/idea-plugin/build.gradle index 47b835b17..3370bc04e 100644 --- a/idea-plugin/build.gradle +++ b/idea-plugin/build.gradle @@ -23,7 +23,7 @@ def name = "palantir-java-format" intellij { pluginName = name updateSinceUntilBuild = true - version = "IU-202.6397.94" + version = "2024.1" plugins = ['java'] } @@ -39,7 +39,7 @@ tasks.named("runIde") { patchPluginXml { pluginDescription = "Formats source code using the palantir-java-format tool." version = project.version - sinceBuild = '193' // TODO: test against this version of IntelliJ to ensure no regressions + sinceBuild = '213' // TODO: test against this version of IntelliJ to ensure no regressions untilBuild = '' } @@ -73,6 +73,7 @@ dependencies { testImplementation 'org.assertj:assertj-core' testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation 'org.junit.platform:junit-platform-launcher' } tasks.withType(JavaCompile).configureEach { diff --git a/idea-plugin/src/main/java/com/palantir/javaformat/intellij/CodeStyleManagerDecorator.java b/idea-plugin/src/main/java/com/palantir/javaformat/intellij/CodeStyleManagerDecorator.java deleted file mode 100644 index b7b403bd3..000000000 --- a/idea-plugin/src/main/java/com/palantir/javaformat/intellij/CodeStyleManagerDecorator.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright 2015 Google Inc. All Rights Reserved. - * - * 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.palantir.javaformat.intellij; - -import com.intellij.formatting.FormattingMode; -import com.intellij.lang.ASTNode; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.fileTypes.FileType; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.Computable; -import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.codeStyle.ChangedRangesInfo; -import com.intellij.psi.codeStyle.CodeStyleManager; -import com.intellij.psi.codeStyle.DocCommentSettings; -import com.intellij.psi.codeStyle.FormattingModeAwareIndentAdjuster; -import com.intellij.psi.codeStyle.Indent; -import com.intellij.psi.impl.source.codeStyle.CodeStyleManagerImpl; -import com.intellij.util.IncorrectOperationException; -import com.intellij.util.ThrowableRunnable; -import java.util.Collection; -import javax.annotation.Nullable; - -/** - * Decorates the {@link CodeStyleManager} abstract class by delegating to a concrete implementation instance (likely - * IJ's default instance). - * - * Explicitly extend the IntelliJ {@link #CodeStyleManagerImpl} so that any methods added in newer releases are - * inherited automatically with a reasonable default implementation. - * - * The https://github.com/JetBrains/intellij-community/commit/2d5740176cc9206db2d5ab5d8f67cec74b85a017 - * added a CodeManager#scheduleReformatWhenSettingsComputed(PsiFile) method in idea/202.5103.13 where the - * default implementation throws an UnsupportedOperationException. - * See https://youtrack.jetbrains.com/issue/IDEA-244645 for more details. - */ -@SuppressWarnings("deprecation") -class CodeStyleManagerDecorator extends CodeStyleManagerImpl implements FormattingModeAwareIndentAdjuster { - - private final CodeStyleManager delegate; - - CodeStyleManagerDecorator(Project project, CodeStyleManager delegate) { - super(project); - this.delegate = delegate; - } - - @Override - public Project getProject() { - return delegate.getProject(); - } - - @Override - public PsiElement reformat(PsiElement element) throws IncorrectOperationException { - return delegate.reformat(element); - } - - @Override - public PsiElement reformat(PsiElement element, boolean canChangeWhiteSpacesOnly) - throws IncorrectOperationException { - return delegate.reformat(element, canChangeWhiteSpacesOnly); - } - - @Override - public PsiElement reformatRange(PsiElement element, int startOffset, int endOffset) - throws IncorrectOperationException { - return delegate.reformatRange(element, startOffset, endOffset); - } - - @Override - public PsiElement reformatRange( - PsiElement element, int startOffset, int endOffset, boolean canChangeWhiteSpacesOnly) - throws IncorrectOperationException { - return delegate.reformatRange(element, startOffset, endOffset, canChangeWhiteSpacesOnly); - } - - @Override - public void reformatText(PsiFile file, int startOffset, int endOffset) throws IncorrectOperationException { - delegate.reformatText(file, startOffset, endOffset); - } - - @Override - public void reformatText(PsiFile file, Collection ranges) throws IncorrectOperationException { - delegate.reformatText(file, ranges); - } - - @Override - public void reformatTextWithContext(PsiFile psiFile, ChangedRangesInfo changedRangesInfo) - throws IncorrectOperationException { - delegate.reformatTextWithContext(psiFile, changedRangesInfo); - } - - @Override - public void reformatTextWithContext(PsiFile file, Collection ranges) throws IncorrectOperationException { - delegate.reformatTextWithContext(file, ranges); - } - - @Override - public void adjustLineIndent(PsiFile file, TextRange rangeToAdjust) throws IncorrectOperationException { - delegate.adjustLineIndent(file, rangeToAdjust); - } - - @Override - public int adjustLineIndent(PsiFile file, int offset) throws IncorrectOperationException { - return delegate.adjustLineIndent(file, offset); - } - - @Override - public int adjustLineIndent(Document document, int offset) { - return delegate.adjustLineIndent(document, offset); - } - - @Override - public void scheduleIndentAdjustment(Document document, int offset) { - delegate.scheduleIndentAdjustment(document, offset); - } - - @Override - public boolean isLineToBeIndented(PsiFile file, int offset) { - return delegate.isLineToBeIndented(file, offset); - } - - @Override - @Nullable - public String getLineIndent(PsiFile file, int offset) { - return delegate.getLineIndent(file, offset); - } - - @Override - @Nullable - public String getLineIndent(PsiFile file, int offset, FormattingMode mode) { - return delegate.getLineIndent(file, offset, mode); - } - - @Override - @Nullable - public String getLineIndent(Document document, int offset) { - return delegate.getLineIndent(document, offset); - } - - @Override - public Indent getIndent(String text, FileType fileType) { - return delegate.getIndent(text, fileType); - } - - @Override - public String fillIndent(Indent indent, FileType fileType) { - return delegate.fillIndent(indent, fileType); - } - - @Override - public Indent zeroIndent() { - return delegate.zeroIndent(); - } - - @Override - public void reformatNewlyAddedElement(ASTNode parent, ASTNode addedElement) throws IncorrectOperationException { - delegate.reformatNewlyAddedElement(parent, addedElement); - } - - @Override - public boolean isSequentialProcessingAllowed() { - return delegate.isSequentialProcessingAllowed(); - } - - @Override - public void performActionWithFormatterDisabled(Runnable r) { - delegate.performActionWithFormatterDisabled(r); - } - - @Override - public void performActionWithFormatterDisabled(ThrowableRunnable r) throws T { - delegate.performActionWithFormatterDisabled(r); - } - - @Override - public T performActionWithFormatterDisabled(Computable r) { - return delegate.performActionWithFormatterDisabled(r); - } - - @Override - public int getSpacing(PsiFile file, int offset) { - return delegate.getSpacing(file, offset); - } - - @Override - public int getMinLineFeeds(PsiFile file, int offset) { - return delegate.getMinLineFeeds(file, offset); - } - - @Override - public void runWithDocCommentFormattingDisabled(PsiFile file, Runnable runnable) { - delegate.runWithDocCommentFormattingDisabled(file, runnable); - } - - @Override - public DocCommentSettings getDocCommentSettings(PsiFile file) { - return delegate.getDocCommentSettings(file); - } - - // From FormattingModeAwareIndentAdjuster - - /** Uses same fallback as {@link CodeStyleManager#getCurrentFormattingMode}. */ - @Override - public FormattingMode getCurrentFormattingMode() { - if (delegate instanceof FormattingModeAwareIndentAdjuster) { - return ((FormattingModeAwareIndentAdjuster) delegate).getCurrentFormattingMode(); - } - return FormattingMode.REFORMAT; - } - - @Override - public int adjustLineIndent(final Document document, final int offset, FormattingMode mode) - throws IncorrectOperationException { - if (delegate instanceof FormattingModeAwareIndentAdjuster) { - return ((FormattingModeAwareIndentAdjuster) delegate).adjustLineIndent(document, offset, mode); - } - return offset; - } -} diff --git a/idea-plugin/src/main/java/com/palantir/javaformat/intellij/FormatterProvider.java b/idea-plugin/src/main/java/com/palantir/javaformat/intellij/FormatterProvider.java index d05f37661..dd1228461 100644 --- a/idea-plugin/src/main/java/com/palantir/javaformat/intellij/FormatterProvider.java +++ b/idea-plugin/src/main/java/com/palantir/javaformat/intellij/FormatterProvider.java @@ -19,7 +19,11 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.intellij.ide.plugins.IdeaPluginDescriptor; +import com.intellij.ide.plugins.PluginManager; import com.intellij.openapi.application.ApplicationInfo; +import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.JdkUtil; import com.intellij.openapi.projectRoots.Sdk; @@ -28,7 +32,6 @@ import com.intellij.openapi.util.SystemInfo; import com.palantir.javaformat.bootstrap.BootstrappingFormatterService; import com.palantir.javaformat.java.FormatterService; -import com.palantir.logsafe.Preconditions; import java.io.IOException; import java.net.MalformedURLException; import java.net.URI; @@ -50,6 +53,8 @@ final class FormatterProvider { private static final Logger log = LoggerFactory.getLogger(FormatterProvider.class); + private static final String PLUGIN_ID = "palantir-java-format"; + // Cache to avoid creating a URLClassloader every time we want to format from IntelliJ private final LoadingCache> implementationCache = Caffeine.newBuilder().maximumSize(1).build(FormatterProvider::createFormatter); @@ -104,7 +109,9 @@ private static List getProvidedImplementationUrls(List implementation private static List getBundledImplementationUrls() { // Load from the jars bundled with the plugin. - Path implDir = PalantirCodeStyleManager.PLUGIN.getPath().toPath().resolve("impl"); + IdeaPluginDescriptor ourPlugin = Preconditions.checkNotNull( + PluginManager.getPlugin(PluginId.getId(PLUGIN_ID)), "Couldn't find our own plugin: %s", PLUGIN_ID); + Path implDir = ourPlugin.getPath().toPath().resolve("impl"); log.debug("Using palantir-java-format implementation bundled with plugin: {}", implDir); return listDirAsUrlsUnchecked(implDir); } diff --git a/idea-plugin/src/main/java/com/palantir/javaformat/intellij/Notifications.java b/idea-plugin/src/main/java/com/palantir/javaformat/intellij/Notifications.java new file mode 100644 index 000000000..8cd505399 --- /dev/null +++ b/idea-plugin/src/main/java/com/palantir/javaformat/intellij/Notifications.java @@ -0,0 +1,39 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.javaformat.intellij; + +import com.intellij.formatting.service.FormattingNotificationService; +import com.intellij.openapi.project.Project; + +class Notifications { + + static final String GENERIC_ERROR_NOTIFICATION_GROUP = "palantir-java-format error"; + static final String PARSING_ERROR_NOTIFICATION_GROUP = "palantir-java-format parsing error"; + static final String PARSING_ERROR_TITLE = PARSING_ERROR_NOTIFICATION_GROUP; + + static String parsingErrorMessage(String filename) { + return "palantir-java-format failed. Does " + filename + " have syntax errors?"; + } + + static void displayParsingErrorNotification(Project project, String filename) { + FormattingNotificationService.getInstance(project) + .reportError( + Notifications.PARSING_ERROR_NOTIFICATION_GROUP, + Notifications.PARSING_ERROR_TITLE, + Notifications.parsingErrorMessage(filename)); + } +} diff --git a/idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirCodeStyleManager.java b/idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirCodeStyleManager.java deleted file mode 100644 index 7d683027b..000000000 --- a/idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirCodeStyleManager.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Copyright 2015 Google Inc. All Rights Reserved. - * - * 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.palantir.javaformat.intellij; - -import static com.google.common.base.Preconditions.checkState; -import static java.util.Comparator.comparing; - -import com.google.common.base.Preconditions; -import com.google.common.collect.BoundType; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Range; -import com.intellij.ide.plugins.IdeaPluginDescriptor; -import com.intellij.ide.plugins.PluginManager; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.command.WriteCommandAction; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.extensions.PluginId; -import com.intellij.openapi.fileTypes.StdFileTypes; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiDocumentManager; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.codeStyle.ChangedRangesInfo; -import com.intellij.psi.codeStyle.CodeStyleManager; -import com.intellij.psi.impl.CheckUtil; -import com.intellij.psi.impl.source.codeStyle.CodeStyleManagerImpl; -import com.intellij.serviceContainer.NonInjectable; -import com.intellij.util.IncorrectOperationException; -import com.palantir.javaformat.java.FormatterException; -import com.palantir.javaformat.java.FormatterService; -import java.util.Collection; -import java.util.Map; -import java.util.Optional; -import java.util.TreeMap; -import java.util.stream.Collectors; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A {@link CodeStyleManager} implementation which formats .java files with palantir-java-format. Formatting of all - * other types of files is delegated to IJ's default implementation. - */ -@SuppressWarnings("PreferSafeLogger") -final class PalantirCodeStyleManager extends CodeStyleManagerDecorator { - private static final Logger log = LoggerFactory.getLogger(PalantirCodeStyleManager.class); - private static final String PLUGIN_ID = "palantir-java-format"; - static final IdeaPluginDescriptor PLUGIN = Preconditions.checkNotNull( - PluginManager.getPlugin(PluginId.getId(PLUGIN_ID)), "Couldn't find our own plugin: %s", PLUGIN_ID); - - private final FormatterProvider formatterProvider = new FormatterProvider(); - - private final Project project; - - public PalantirCodeStyleManager(@NotNull Project project) { - super(project, new CodeStyleManagerImpl(project)); - this.project = project; - } - - @NonInjectable - PalantirCodeStyleManager(@NotNull CodeStyleManager original) { - super(original.getProject(), original); - this.project = original.getProject(); - } - - @SuppressWarnings("ImmutableMapDuplicateKeyStrategy") - static Map getReplacements( - FormatterService formatter, String text, Collection ranges) { - try { - ImmutableMap.Builder replacements = ImmutableMap.builder(); - formatter.getFormatReplacements(text, toRanges(ranges)).forEach(replacement -> { - replacements.put(toTextRange(replacement.getReplaceRange()), replacement.getReplacementString()); - }); - return replacements.build(); - } catch (FormatterException e) { - log.debug("Formatter failed, no replacements", e); - return ImmutableMap.of(); - } - } - - private static Collection> toRanges(Collection textRanges) { - return textRanges.stream() - .map(textRange -> Range.closedOpen(textRange.getStartOffset(), textRange.getEndOffset())) - .collect(Collectors.toList()); - } - - private static TextRange toTextRange(Range range) { - checkState(range.lowerBoundType().equals(BoundType.CLOSED) - && range.upperBoundType().equals(BoundType.OPEN)); - return new TextRange(range.lowerEndpoint(), range.upperEndpoint()); - } - - @Override - public void reformatText(PsiFile file, int startOffset, int endOffset) throws IncorrectOperationException { - if (overrideFormatterForFile(file)) { - formatInternal(file, ImmutableList.of(new TextRange(startOffset, endOffset))); - } else { - super.reformatText(file, startOffset, endOffset); - } - } - - @Override - public void reformatText(PsiFile file, Collection ranges) throws IncorrectOperationException { - if (overrideFormatterForFile(file)) { - formatInternal(file, ranges); - } else { - super.reformatText(file, ranges); - } - } - - @Override - public void reformatTextWithContext(PsiFile psiFile, ChangedRangesInfo changedRangesInfo) - throws IncorrectOperationException { - reformatTextWithContext(psiFile, changedRangesInfo.allChangedRanges); - } - - @Override - public void reformatTextWithContext(PsiFile file, Collection ranges) { - if (overrideFormatterForFile(file)) { - formatInternal(file, ranges); - } else { - super.reformatTextWithContext(file, ranges); - } - } - - @Override - public PsiElement reformatRange(PsiElement element, int startOffset, int endOffset) - throws IncorrectOperationException { - // Preserve the fallback defined in CodeStyleManagerImpl - return reformatRange(element, startOffset, endOffset, false); - } - - @Override - public PsiElement reformatRange( - PsiElement element, int startOffset, int endOffset, boolean canChangeWhiteSpacesOnly) { - // Only handle elements that are PsiFile for now -- otherwise we need to search for some - // element within the file at new locations given the original startOffset and endOffsets - // to serve as the return value. - PsiFile file = element instanceof PsiFile ? (PsiFile) element : null; - if (file != null && canChangeWhiteSpacesOnly && overrideFormatterForFile(file)) { - formatInternal(file, ImmutableList.of(new TextRange(startOffset, endOffset))); - return file; - } else { - return super.reformatRange(element, startOffset, endOffset, canChangeWhiteSpacesOnly); - } - } - - /** Return whether or not this formatter can handle formatting the given file. */ - private boolean overrideFormatterForFile(PsiFile file) { - return StdFileTypes.JAVA.equals(file.getFileType()) - && PalantirJavaFormatSettings.getInstance(getProject()).isEnabled(); - } - - private void formatInternal(PsiFile file, Collection ranges) { - ApplicationManager.getApplication().assertWriteAccessAllowed(); - PsiDocumentManager documentManager = PsiDocumentManager.getInstance(getProject()); - documentManager.commitAllDocuments(); - CheckUtil.checkWritable(file); - - Document document = documentManager.getDocument(file); - - if (document == null) { - return; - } - // If there are postponed PSI changes (e.g., during a refactoring), just abort. - // If we apply them now, then the incoming text ranges may no longer be valid. - if (documentManager.isDocumentBlockedByPsi(document)) { - return; - } - - format(document, ranges); - } - - /** - * Format the ranges of the given document. - * - *

Overriding methods will need to modify the document with the result of the external formatter (usually using - * {@link #performReplacements(Document, Map)}. - */ - private void format(Document document, Collection ranges) { - PalantirJavaFormatSettings settings = PalantirJavaFormatSettings.getInstance(getProject()); - Optional formatter = formatterProvider.get(project, settings); - if (formatter.isEmpty()) { - log.warn("Could not find a formatter! Making no changes"); - return; - } - - performReplacements(document, getReplacements(formatter.get(), document.getText(), ranges)); - } - - private void performReplacements(final Document document, final Map replacements) { - if (replacements.isEmpty()) { - return; - } - - TreeMap sorted = new TreeMap<>(comparing(TextRange::getStartOffset)); - sorted.putAll(replacements); - WriteCommandAction.runWriteCommandAction(getProject(), () -> { - for (Map.Entry entry : sorted.descendingMap().entrySet()) { - document.replaceString( - entry.getKey().getStartOffset(), entry.getKey().getEndOffset(), entry.getValue()); - } - PsiDocumentManager.getInstance(getProject()).commitDocument(document); - }); - } -} diff --git a/idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatFormattingService.java b/idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatFormattingService.java new file mode 100644 index 000000000..6ddbe30b9 --- /dev/null +++ b/idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatFormattingService.java @@ -0,0 +1,157 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.javaformat.intellij; + +import static java.util.Comparator.comparing; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Range; +import com.intellij.formatting.service.AsyncDocumentFormattingService; +import com.intellij.formatting.service.AsyncFormattingRequest; +import com.intellij.ide.highlighter.JavaFileType; +import com.intellij.lang.ImportOptimizer; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.NlsSafe; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiFile; +import com.palantir.javaformat.java.FormatterException; +import com.palantir.javaformat.java.FormatterService; +import com.palantir.javaformat.java.Replacement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.jetbrains.annotations.NotNull; + +class PalantirJavaFormatFormattingService extends AsyncDocumentFormattingService { + private final FormatterProvider formatterProvider = new FormatterProvider(); + + @Override + protected FormattingTask createFormattingTask(@NotNull AsyncFormattingRequest request) { + Project project = request.getContext().getProject(); + PalantirJavaFormatSettings settings = PalantirJavaFormatSettings.getInstance(project); + Optional formatter = formatterProvider.get(project, settings); + return new PalantirJavaFormatFormattingTask(request, formatter); + } + + @Override + protected @NotNull String getNotificationGroupId() { + return Notifications.PARSING_ERROR_NOTIFICATION_GROUP; + } + + @Override + protected @NotNull @NlsSafe String getName() { + return "palantir-java-format"; + } + + @Override + public @NotNull Set getFeatures() { + return Set.of(Feature.AD_HOC_FORMATTING, Feature.FORMAT_FRAGMENTS, Feature.OPTIMIZE_IMPORTS); + } + + @Override + public boolean canFormat(@NotNull PsiFile file) { + return JavaFileType.INSTANCE.equals(file.getFileType()) + && PalantirJavaFormatSettings.getInstance(file.getProject()).isEnabled(); + } + + @Override + public @NotNull Set getImportOptimizers(@NotNull PsiFile file) { + Project project = file.getProject(); + PalantirJavaFormatSettings settings = PalantirJavaFormatSettings.getInstance(project); + Optional formatter = formatterProvider.get(project, settings); + return Set.of(new PalantirJavaFormatImportOptimizer(formatter)); + } + + private static final class PalantirJavaFormatFormattingTask implements FormattingTask { + private final AsyncFormattingRequest request; + private final Optional formatterService; + + public PalantirJavaFormatFormattingTask( + AsyncFormattingRequest request, Optional formatterService) { + this.request = request; + this.formatterService = formatterService; + } + + @Override + public void run() { + if (formatterService.isEmpty()) { + request.onError( + Notifications.GENERIC_ERROR_NOTIFICATION_GROUP, + "Failed to format file because formatterService is not configured"); + return; + } + + try { + String formattedText = applyReplacements( + request.getDocumentText(), + formatterService.get().getFormatReplacements(request.getDocumentText(), toRanges(request))); + request.onTextReady(formattedText); + } catch (FormatterException e) { + request.onError( + Notifications.PARSING_ERROR_TITLE, + Notifications.parsingErrorMessage( + request.getContext().getContainingFile().getName())); + } + } + + public static String applyReplacements(String input, Collection replacementsCollection) { + List replacements = new ArrayList<>(replacementsCollection); + replacements.sort(comparing((Replacement r) -> r.getReplaceRange().lowerEndpoint()) + .reversed()); + StringBuilder writer = new StringBuilder(input); + for (Replacement replacement : replacements) { + writer.replace( + replacement.getReplaceRange().lowerEndpoint(), + replacement.getReplaceRange().upperEndpoint(), + replacement.getReplacementString()); + } + return writer.toString(); + } + + private static Collection> toRanges(AsyncFormattingRequest request) { + if (isWholeFile(request)) { + // The IDE sometimes passes invalid ranges when the file is unsaved before invoking the + // formatter. So this is a workaround for that issue. + return ImmutableList.of( + Range.closedOpen(0, request.getDocumentText().length())); + } + return request.getFormattingRanges().stream() + .map(textRange -> Range.closedOpen(textRange.getStartOffset(), textRange.getEndOffset())) + .collect(ImmutableList.toImmutableList()); + } + + private static boolean isWholeFile(AsyncFormattingRequest request) { + List ranges = request.getFormattingRanges(); + return ranges.size() == 1 + && ranges.get(0).getStartOffset() == 0 + // using greater than or equal because ranges are sometimes passed inaccurately + && ranges.get(0).getEndOffset() >= request.getDocumentText().length(); + } + + @Override + public boolean isRunUnderProgress() { + return true; + } + + @Override + public boolean cancel() { + return false; + } + } +} diff --git a/idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatImportOptimizer.java b/idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatImportOptimizer.java new file mode 100644 index 000000000..d07eb11a7 --- /dev/null +++ b/idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatImportOptimizer.java @@ -0,0 +1,92 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.javaformat.intellij; + +import com.google.common.util.concurrent.Runnables; +import com.intellij.ide.highlighter.JavaFileType; +import com.intellij.lang.ImportOptimizer; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiFile; +import com.palantir.javaformat.java.FormatterException; +import com.palantir.javaformat.java.FormatterService; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; + +public class PalantirJavaFormatImportOptimizer implements ImportOptimizer { + + private final Optional formatterService; + + public PalantirJavaFormatImportOptimizer(Optional formatterService) { + this.formatterService = formatterService; + } + + @Override + public boolean supports(@NotNull PsiFile file) { + return JavaFileType.INSTANCE.equals(file.getFileType()) + && PalantirJavaFormatSettings.getInstance(file.getProject()).isEnabled(); + } + + @Override + public @NotNull Runnable processFile(@NotNull PsiFile file) { + if (formatterService.isEmpty()) { + Notifications.displayParsingErrorNotification(file.getProject(), file.getName()); + return Runnables.doNothing(); + } + Project project = file.getProject(); + + PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project); + Document document = documentManager.getDocument(file); + + if (document == null) { + return Runnables.doNothing(); + } + + final String origText = document.getText(); + String text; + try { + text = formatterService.get().fixImports(origText); + } catch (FormatterException e) { + Notifications.displayParsingErrorNotification(project, file.getName()); + return Runnables.doNothing(); + } + + // pointless to change document text if it hasn't changed, plus this can interfere with + // e.g. PalantirJavaFormattingService's output, i.e. it can overwrite the results from the main + // formatter. + if (text.equals(origText)) { + return Runnables.doNothing(); + } + + return () -> { + if (documentManager.isDocumentBlockedByPsi(document)) { + documentManager.doPostponedOperationsAndUnblockDocument(document); + } + + // similarly to above, don't overwrite new document text if it has changed - we use + // getCharsSequence() as we should have `writeAction()` (which I think means effectively a + // write-lock) and it saves calling getText(), which apparently is expensive. + CharSequence newText = document.getCharsSequence(); + if (CharSequence.compare(origText, newText) != 0) { + return; + } + + document.setText(text); + }; + } +} diff --git a/idea-plugin/src/main/resources/META-INF/plugin.xml b/idea-plugin/src/main/resources/META-INF/plugin.xml index 22f1e563a..ae5280e2e 100644 --- a/idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/idea-plugin/src/main/resources/META-INF/plugin.xml @@ -5,20 +5,20 @@ Palantir - com.intellij.modules.java + com.intellij.modules.lang + com.intellij.modules.platform + - + diff --git a/idea-plugin/src/test/java/com/palantir/javaformat/intellij/PalantirJavaFormatFormattingServiceTest.java b/idea-plugin/src/test/java/com/palantir/javaformat/intellij/PalantirJavaFormatFormattingServiceTest.java new file mode 100644 index 000000000..7e617f36a --- /dev/null +++ b/idea-plugin/src/test/java/com/palantir/javaformat/intellij/PalantirJavaFormatFormattingServiceTest.java @@ -0,0 +1,152 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.javaformat.intellij; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableList; +import com.intellij.codeInsight.actions.ReformatCodeProcessor; +import com.intellij.formatting.service.AsyncFormattingRequest; +import com.intellij.formatting.service.FormattingService; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.JavaSdk; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiFile; +import com.intellij.testFramework.ExtensionTestUtil; +import com.intellij.testFramework.LightProjectDescriptor; +import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor; +import com.intellij.testFramework.fixtures.IdeaProjectTestFixture; +import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory; +import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture; +import com.intellij.testFramework.fixtures.JavaTestFixtureFactory; +import com.intellij.testFramework.fixtures.TestFixtureBuilder; +import com.palantir.javaformat.intellij.PalantirJavaFormatSettings.State; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class PalantirJavaFormatFormattingServiceTest { + private JavaCodeInsightTestFixture fixture; + private PalantirJavaFormatSettings settings; + private DelegatingFormatter delegatingFormatter; + + @BeforeEach + public void setUp() throws Exception { + TestFixtureBuilder projectBuilder = IdeaTestFixtureFactory.getFixtureFactory() + .createLightFixtureBuilder(getProjectDescriptor(), getClass().getName()); + fixture = JavaTestFixtureFactory.getFixtureFactory().createCodeInsightFixture(projectBuilder.getFixture()); + fixture.setUp(); + + delegatingFormatter = new DelegatingFormatter(); + ExtensionTestUtil.maskExtensions( + FormattingService.EP_NAME, ImmutableList.of(delegatingFormatter), fixture.getProjectDisposable()); + + settings = PalantirJavaFormatSettings.getInstance(fixture.getProject()); + State resetState = new State(); + resetState.setEnabled("true"); + settings.loadState(resetState); + } + + @AfterEach + public void tearDown() throws Exception { + fixture.tearDown(); + } + + @Test + public void defaultFormatSettings() throws Exception { + String input = Files.readString( + Paths.get("../palantir-java-format/src/test/resources/com/palantir/javaformat/java/testdata/A.input")); + String output = Files.readString( + Path.of("../palantir-java-format/src/test/resources/com/palantir/javaformat/java/testdata/A.output")); + PsiFile file = createPsiFile("com/foo/FormatTest.java", input); + ReformatCodeProcessor processor = new ReformatCodeProcessor(file, false); + WriteCommandAction.runWriteCommandAction(file.getProject(), () -> { + ProjectRootManager.getInstance(getProject()) + .setProjectSdk(getProjectDescriptor().getSdk()); + + processor.run(); + PsiDocumentManager.getInstance(file.getProject()).commitAllDocuments(); + }); + + assertThat(file.getText()).isEqualTo(output); + assertThat(delegatingFormatter.wasInvoked()).isTrue(); + } + + protected Project getProject() { + return fixture.getProject(); + } + + @NotNull + protected LightProjectDescriptor getProjectDescriptor() { + return new DefaultLightProjectDescriptor() { + @Override + public Sdk getSdk() { + try { + return JavaSdk.getInstance() + .createJdk( + "java 1.11", new File(System.getProperty("java.home")).getCanonicalPath(), false); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }; + } + + private PsiFile createPsiFile(String path, String... contents) throws IOException { + VirtualFile virtualFile = fixture.getTempDirFixture().createFile(path, String.join("\n", contents)); + fixture.configureFromExistingVirtualFile(virtualFile); + PsiFile psiFile = fixture.getFile(); + assertThat(psiFile).isNotNull(); + return psiFile; + } + + private static final class DelegatingFormatter extends PalantirJavaFormatFormattingService { + + private boolean invoked = false; + + private boolean wasInvoked() { + return invoked; + } + + @Override + protected FormattingTask createFormattingTask(AsyncFormattingRequest request) { + FormattingTask delegateTask = super.createFormattingTask(request); + return new FormattingTask() { + @Override + public boolean cancel() { + return delegateTask.cancel(); + } + + @Override + public void run() { + invoked = true; + delegateTask.run(); + } + }; + } + } +} diff --git a/idea-plugin/src/test/java/com/palantir/javaformat/intellij/PalantirJavaFormatImportOptimizerTest.java b/idea-plugin/src/test/java/com/palantir/javaformat/intellij/PalantirJavaFormatImportOptimizerTest.java new file mode 100644 index 000000000..5b0977318 --- /dev/null +++ b/idea-plugin/src/test/java/com/palantir/javaformat/intellij/PalantirJavaFormatImportOptimizerTest.java @@ -0,0 +1,185 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.javaformat.intellij; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableList; +import com.intellij.codeInsight.actions.OptimizeImportsProcessor; +import com.intellij.formatting.service.FormattingService; +import com.intellij.lang.ImportOptimizer; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.JavaSdk; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiFile; +import com.intellij.testFramework.ExtensionTestUtil; +import com.intellij.testFramework.LightProjectDescriptor; +import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor; +import com.intellij.testFramework.fixtures.IdeaProjectTestFixture; +import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory; +import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture; +import com.intellij.testFramework.fixtures.JavaTestFixtureFactory; +import com.intellij.testFramework.fixtures.TestFixtureBuilder; +import com.palantir.javaformat.intellij.PalantirJavaFormatSettings.State; +import java.io.File; +import java.io.IOException; +import java.util.Set; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class PalantirJavaFormatImportOptimizerTest { + private JavaCodeInsightTestFixture fixture; + private DelegatingFormatter delegatingFormatter; + + @BeforeEach + public void setUp() throws Exception { + TestFixtureBuilder projectBuilder = IdeaTestFixtureFactory.getFixtureFactory() + .createLightFixtureBuilder(getProjectDescriptor(), getClass().getName()); + fixture = JavaTestFixtureFactory.getFixtureFactory().createCodeInsightFixture(projectBuilder.getFixture()); + fixture.setUp(); + + delegatingFormatter = new DelegatingFormatter(); + ExtensionTestUtil.maskExtensions( + FormattingService.EP_NAME, ImmutableList.of(delegatingFormatter), fixture.getProjectDisposable()); + + PalantirJavaFormatSettings settings = PalantirJavaFormatSettings.getInstance(fixture.getProject()); + State resetState = new State(); + resetState.setEnabled("true"); + settings.loadState(resetState); + } + + protected Project getProject() { + return fixture.getProject(); + } + + @NotNull + protected LightProjectDescriptor getProjectDescriptor() { + return new DefaultLightProjectDescriptor() { + @Override + public Sdk getSdk() { + try { + return JavaSdk.getInstance() + .createJdk( + "java 1.11", new File(System.getProperty("java.home")).getCanonicalPath(), false); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }; + } + + @AfterEach + public void tearDown() throws Exception { + fixture.tearDown(); + } + + @Test + public void removesUnusedImports() throws Exception { + PsiFile file = createPsiFile( + "com/foo/ImportTest.java", + "package com.foo;", + "import java.util.List;", + "import java.util.ArrayList;", + "import java.util.Map;", + "public class ImportTest {", + "static final Map map;", + "}"); + OptimizeImportsProcessor processor = new OptimizeImportsProcessor(file.getProject(), file); + WriteCommandAction.runWriteCommandAction(file.getProject(), () -> { + ProjectRootManager.getInstance(getProject()) + .setProjectSdk(getProjectDescriptor().getSdk()); + processor.run(); + PsiDocumentManager.getInstance(file.getProject()).commitAllDocuments(); + }); + + assertThat(file.getText()).doesNotContain("List"); + assertThat(file.getText()).contains("import java.util.Map;"); + assertThat(delegatingFormatter.wasInvoked()).isTrue(); + } + + @Test + public void reordersImports() throws Exception { + PsiFile file = createPsiFile( + "com/foo/ImportTest.java", + "package com.foo;", + "import java.util.List;", + "import java.util.ArrayList;", + "import java.util.Map;", + "public class ImportTest {", + "static final ArrayList arrayList;", + "static final List list;", + "static final Map map;", + "}"); + OptimizeImportsProcessor processor = new OptimizeImportsProcessor(file.getProject(), file); + WriteCommandAction.runWriteCommandAction(file.getProject(), () -> { + ProjectRootManager.getInstance(getProject()) + .setProjectSdk(getProjectDescriptor().getSdk()); + processor.run(); + PsiDocumentManager.getInstance(file.getProject()).commitAllDocuments(); + }); + + assertThat(file.getText()) + .contains("import java.util.ArrayList;\n" + "import java.util.List;\n" + "import java.util.Map;\n"); + assertThat(delegatingFormatter.wasInvoked()).isTrue(); + } + + private PsiFile createPsiFile(String path, String... contents) throws IOException { + VirtualFile virtualFile = fixture.getTempDirFixture().createFile(path, String.join("\n", contents)); + fixture.configureFromExistingVirtualFile(virtualFile); + PsiFile psiFile = fixture.getFile(); + assertThat(psiFile).isNotNull(); + return psiFile; + } + + private static final class DelegatingFormatter extends PalantirJavaFormatFormattingService { + + private boolean invoked = false; + + private boolean wasInvoked() { + return invoked; + } + + @Override + public @NotNull Set getImportOptimizers(@NotNull PsiFile file) { + return super.getImportOptimizers(file).stream().map(this::wrap).collect(Collectors.toUnmodifiableSet()); + } + + private ImportOptimizer wrap(ImportOptimizer delegate) { + return new ImportOptimizer() { + @Override + public boolean supports(@NotNull PsiFile file) { + return delegate.supports(file); + } + + @Override + public @NotNull Runnable processFile(@NotNull PsiFile file) { + return () -> { + invoked = true; + delegate.processFile(file).run(); + }; + } + }; + } + } +} diff --git a/palantir-java-format-jdk-bootstrap/src/main/java/com/palantir/javaformat/bootstrap/BootstrappingFormatterService.java b/palantir-java-format-jdk-bootstrap/src/main/java/com/palantir/javaformat/bootstrap/BootstrappingFormatterService.java index 5fffc9113..5c88f5876 100644 --- a/palantir-java-format-jdk-bootstrap/src/main/java/com/palantir/javaformat/bootstrap/BootstrappingFormatterService.java +++ b/palantir-java-format-jdk-bootstrap/src/main/java/com/palantir/javaformat/bootstrap/BootstrappingFormatterService.java @@ -24,6 +24,7 @@ import com.google.common.collect.BoundType; import com.google.common.collect.ImmutableList; import com.google.common.collect.Range; +import com.palantir.javaformat.java.FormatterException; import com.palantir.javaformat.java.FormatterService; import com.palantir.javaformat.java.Replacement; import java.io.IOException; @@ -62,7 +63,16 @@ public ImmutableList getFormatReplacements(String input, Collection @Override public String formatSourceReflowStringsAndFixImports(String input) { try { - return formatSourceReflowStringsAndFixImportsInternal(input); + return runFormatterCommand(input); + } catch (IOException e) { + throw new RuntimeException("Error running formatter command", e); + } + } + + @Override + public String fixImports(String input) throws FormatterException { + try { + return runFormatterCommand(input); } catch (IOException e) { throw new RuntimeException("Error running formatter command", e); } @@ -87,7 +97,7 @@ private ImmutableList getFormatReplacementsInternal(String input, C return MAPPER.readValue(output.get(), new TypeReference<>() {}); } - private String formatSourceReflowStringsAndFixImportsInternal(String input) throws IOException { + private String runFormatterCommand(String input) throws IOException { FormatterCliArgs command = FormatterCliArgs.builder() .jdkPath(jdkPath) .withJvmArgsForVersion(jdkMajorVersion) diff --git a/palantir-java-format-spi/src/main/java/com/palantir/javaformat/java/FormatterService.java b/palantir-java-format-spi/src/main/java/com/palantir/javaformat/java/FormatterService.java index 538f0a983..51ef1c419 100644 --- a/palantir-java-format-spi/src/main/java/com/palantir/javaformat/java/FormatterService.java +++ b/palantir-java-format-spi/src/main/java/com/palantir/javaformat/java/FormatterService.java @@ -44,8 +44,15 @@ ImmutableList getFormatReplacements(String input, CollectionGoogle Java - * Style Guide - 3.3.3 Import ordering and spacing */ String formatSourceReflowStringsAndFixImports(String input) throws FormatterException; + + /** + * Fixes imports (eg. ordering, spacing, and removal of unused import statements). + * + * @param input the input string + * @return the output string + * @throws FormatterException if the input string cannot be parsed + */ + String fixImports(String input) throws FormatterException; } diff --git a/palantir-java-format/src/main/java/com/palantir/javaformat/java/Formatter.java b/palantir-java-format/src/main/java/com/palantir/javaformat/java/Formatter.java index 538dc9063..bf07ca2d5 100644 --- a/palantir-java-format/src/main/java/com/palantir/javaformat/java/Formatter.java +++ b/palantir-java-format/src/main/java/com/palantir/javaformat/java/Formatter.java @@ -264,6 +264,19 @@ public String formatSourceAndFixImports(String input) throws FormatterException return formatted; } + /** + * Fixes imports (e.g. ordering, spacing, and removal of unused import statements). + * + * @param input the input string + * @return the output string + * @throws FormatterException if the input string cannot be parsed + * @see Google Java + * Style Guide - 3.3.3 Import ordering and spacing + */ + public String fixImports(String input) throws FormatterException { + return ImportOrderer.reorderImports(RemoveUnusedImports.removeUnusedImports(input), options.style()); + } + /** * Format an input string (a Java compilation unit), for only the specified character ranges. These ranges are * extended as necessary (e.g., to encompass whole lines). diff --git a/palantir-java-format/src/main/java/com/palantir/javaformat/java/FormatterServiceImpl.java b/palantir-java-format/src/main/java/com/palantir/javaformat/java/FormatterServiceImpl.java index b544e0dbb..fb4660882 100644 --- a/palantir-java-format/src/main/java/com/palantir/javaformat/java/FormatterServiceImpl.java +++ b/palantir-java-format/src/main/java/com/palantir/javaformat/java/FormatterServiceImpl.java @@ -42,4 +42,9 @@ public ImmutableList getFormatReplacements(String text, Collection< public String formatSourceReflowStringsAndFixImports(String input) throws FormatterException { return formatter.formatSourceAndFixImports(input); } + + @Override + public String fixImports(String input) throws FormatterException { + return formatter.fixImports(input); + } } diff --git a/versions.props b/versions.props index fdc013e5e..5b9388752 100644 --- a/versions.props +++ b/versions.props @@ -16,6 +16,7 @@ org.functionaljava:functionaljava = 4.8 org.immutables:value = 2.10.1 org.junit.jupiter:* = 5.10.2 org.junit.vintage:* = 5.10.2 +org.junit.platform:* = 1.10.2 org.slf4j:* = 1.7.36 com.fasterxml.jackson.*:* = 2.16.1 com.fasterxml.jackson.core:jackson-databind = 2.16.1