diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseFramework.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseFramework.java index 2eaf9e568e..4292c7914c 100644 --- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseFramework.java +++ b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseFramework.java @@ -53,7 +53,7 @@ public enum DefaultBundles { *

*

* Per default, the platform is not activated. Some plugins use this information - * to determine whether they are running in a headless modes (without IDE). + * to determine whether they are running in a headless mode (without IDE). *

*/ PLATFORM(org.eclipse.core.internal.runtime.PlatformActivator.class, Bundle.RESOLVED), diff --git a/lib-extra/build.gradle b/lib-extra/build.gradle index 491531f0c7..4b8717f4f4 100644 --- a/lib-extra/build.gradle +++ b/lib-extra/build.gradle @@ -85,6 +85,26 @@ p2deps { p2repo 'https://download.eclipse.org/tools/cdt/releases/10.7/' install 'org.eclipse.cdt.core' } + + /* + * JDT core manipulation required for clean-up base interfaces and import sorting + * It depends on JDT core, which is required for formatting. + */ + /**compile("org.eclipse.jdt:org.eclipse.jdt.core.manipulation:${VER_ECLIPSE_JDT_CORE_MANIPULATION}") { + exclude group: 'org.eclipse.jdt', module: 'org.eclipse.jdt.launching' + exclude group: 'org.eclipse.platform', module: 'org.eclipse.ant.core' + exclude group: 'org.eclipse.platform', module: 'org.eclipse.core.expressions' + }*/ + + /* + * JDT UI required for clean-up. + * Only the org.eclipse.jdt.internal.corext.fix package is required. + * All dependencies (like SWT) are excluded. + */ + /**compile("org.eclipse.jdt:org.eclipse.jdt.ui:${VER_ECLIPSE_JDT_UI}") { + exclude group: 'org.eclipse.platform' + exclude group: 'org.eclipse.jdt' + }*/ } // we'll hold the core lib to a high standard diff --git a/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/CleanUpFactory.java b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/CleanUpFactory.java new file mode 100644 index 0000000000..425f569363 --- /dev/null +++ b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/CleanUpFactory.java @@ -0,0 +1,216 @@ +/* + * Copyright 2016 DiffPlug + * + * 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.diffplug.spotless.extra.glue.jdt; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.xpath.XPathExpressionException; + +import org.assertj.core.util.Sets; +import org.eclipse.jdt.internal.corext.fix.CleanUpConstants; +import org.eclipse.jdt.internal.corext.fix.CleanUpConstantsOptions; +import org.eclipse.jdt.ui.cleanup.CleanUpOptions; +import org.eclipse.jdt.ui.cleanup.ICleanUp; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** Provides configured clean-up implementations. */ +final class CleanUpFactory { + + private final static Set UNSUPPORTED_CLASSES = Collections.unmodifiableSet(Sets.newLinkedHashSet( + "org.eclipse.jdt.internal.ui.fix.UnimplementedCodeCleanUp" //Would require Eclipse templates + )); + + @SuppressWarnings("serial") + private final static Map UNSUPPORTED_CONFIG = Collections.unmodifiableMap(new HashMap() { + { + put(CleanUpConstants.REMOVE_UNUSED_CODE_IMPORTS, new FixedValue("false", "Unused import clean-up only works in case all imports can be resolved. As an alternative use: " + CleanUpConstants.ORGANIZE_IMPORTS)); + } + }); + + private final static String CLEAN_UP_CONFIG_FILE_NAME = "plugin.xml"; + private final static String CLEAN_UP_CONFIG_DEPENDENCY_NAME = "org.eclipse.jdt.ui"; + private static List> CLEAN_UP_SEQUENCE = null; + private final CleanUpOptions options; + + CleanUpFactory(Properties settings) { + options = new CleanUpOptions(); + Logger logger = LoggerFactory.getLogger(CleanUpFactory.class); + CleanUpConstantsOptions.setDefaultOptions(CleanUpConstants.DEFAULT_CLEAN_UP_OPTIONS, options); + UNSUPPORTED_CONFIG.entrySet().stream().forEach(entry -> options.setOption(entry.getKey(), entry.getValue().value)); + settings.forEach((key, value) -> { + FixedValue fixed = UNSUPPORTED_CONFIG.get(key); + if (null != fixed && fixed.value != value) { + logger.warn(String.format("Using %s for %s instead of %s: %s", fixed.value, key, value, fixed.reason)); + } else { + options.setOption(key.toString(), value.toString()); + } + }); + try { + initializeCleanupActions(); + } catch (IOException | ParserConfigurationException | XPathExpressionException e) { + throw new RuntimeException("Faild to read Eclipse Clean-Up configuration.", e); + } + } + + private static synchronized void initializeCleanupActions() throws IOException, ParserConfigurationException, XPathExpressionException { + if (null != CLEAN_UP_SEQUENCE) { + return; + } + ClassLoader loader = CleanUpFactory.class.getClassLoader(); + Optional configUrl = Collections.list(loader.getResources(CLEAN_UP_CONFIG_FILE_NAME)).stream().filter(url -> url.getPath().contains(CLEAN_UP_CONFIG_DEPENDENCY_NAME)).findAny(); + if (!configUrl.isPresent()) { + throw new RuntimeException("Could not find JAR containing " + CLEAN_UP_CONFIG_DEPENDENCY_NAME + ":" + CLEAN_UP_CONFIG_FILE_NAME); + } + InputStream configXmlStream = configUrl.get().openStream(); + try { + SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); + CleanUpExtensionHandler handler = new CleanUpExtensionHandler(); + saxParser.parse(configXmlStream, handler); + CLEAN_UP_SEQUENCE = handler.getCleanUpSequence(); + } catch (SAXException e) { + //Add information about the XML location + throw new RuntimeException("Failed to parse " + configUrl.get().toExternalForm(), e); + } + } + + public List create() { + return CLEAN_UP_SEQUENCE.stream().map(constructor -> { + try { + ICleanUp cleanUp = constructor.newInstance(); + cleanUp.setOptions(options); + return cleanUp; + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new RuntimeException("Failed to created clean-up action for " + constructor.getName(), e); + } + }).collect(Collectors.toList()); + } + + private static class FixedValue { + public final String value; + public final String reason; + + FixedValue(String value, String reason) { + this.value = value; + this.reason = reason; + } + }; + + private final static class CleanUpExtensionHandler extends DefaultHandler { + private final static String CLEAN_UP_ELEMENT_NAME = "cleanUp"; + private final static String ID_ATTRIBUTE_NAME = "id"; + private final static String CLASS_ATTRIBUTE_NAME = "class"; + private final static String RUN_AFTER_ATTRIBUTE_NAME = "runAfter"; + private final Map> constructor; + private final Map runAfter; + private final LinkedList sorted; + + CleanUpExtensionHandler() { + constructor = new HashMap<>(); + runAfter = new LinkedHashMap<>(); //E.g. the elements are already sorted + sorted = new LinkedList<>(); + } + + @Override + public void startDocument() throws SAXException { + constructor.clear(); + runAfter.clear(); + sorted.clear(); + super.startDocument(); + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + if (CLEAN_UP_ELEMENT_NAME == qName) { + String id = getMandatoryAttribute(attributes, ID_ATTRIBUTE_NAME); + String className = getMandatoryAttribute(attributes, CLASS_ATTRIBUTE_NAME); + if (!UNSUPPORTED_CLASSES.contains(className)) { + try { + Class clazz = Class.forName(className); + Class clazzImplementsICleanUp = clazz.asSubclass(ICleanUp.class); + constructor.put(id, clazzImplementsICleanUp.getConstructor()); + } catch (ClassNotFoundException | ClassCastException | NoSuchMethodException | SecurityException e) { + throw new SAXException("Failed to obtain constructor for " + CLEAN_UP_ELEMENT_NAME + " element class " + className, e); + } + } + String runAfterId = attributes.getValue(RUN_AFTER_ATTRIBUTE_NAME); + if (null == runAfterId) { + sorted.push(id); + } else { + runAfter.put(id, runAfterId); + } + } + super.startElement(uri, localName, qName, attributes); + } + + private static String getMandatoryAttribute(Attributes attributes, String qName) throws SAXException { + String value = attributes.getValue(qName); + if (null == value) { + throw new SAXException(CLEAN_UP_ELEMENT_NAME + " element without " + qName + " attribute."); + } + return value; + } + + @Override + public void endDocument() throws SAXException { + if (runAfter.isEmpty()) { + throw new SAXException(CLEAN_UP_ELEMENT_NAME + " element has not been found in XML."); + } + while (!runAfter.isEmpty()) { + //E.g. the elements are already sorted. Hence only one iteration is expected. + List foundEntries = new ArrayList<>(runAfter.size()); + for (Map.Entry entry : runAfter.entrySet()) { + int runAfterIndex = sorted.lastIndexOf(entry.getValue()); + if (0 <= runAfterIndex) { + foundEntries.add(entry.getKey()); + sorted.add(runAfterIndex + 1, entry.getKey()); + } + } + foundEntries.forEach(e -> runAfter.remove(e)); + if (foundEntries.isEmpty()) { + throw new SAXException(CLEAN_UP_ELEMENT_NAME + " element the following precessor IDs cannot be resolved: " + runAfter.values().stream().collect(Collectors.joining("; "))); + } + } + super.endDocument(); + } + + public List> getCleanUpSequence() { + return sorted.stream().map(id -> constructor.get(id)).filter(clazz -> null != clazz).collect(Collectors.toList()); + } + } + +} diff --git a/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtCleanUpStepImpl.java b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtCleanUpStepImpl.java new file mode 100644 index 0000000000..5644f1e41e --- /dev/null +++ b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtCleanUpStepImpl.java @@ -0,0 +1,220 @@ +/* + * Copyright 2016 DiffPlug + * + * 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.diffplug.spotless.extra.glue.jdt; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.manipulation.SharedASTProviderCore; +import org.eclipse.jdt.core.refactoring.CompilationUnitChange; +import org.eclipse.jdt.ui.cleanup.CleanUpContext; +import org.eclipse.jdt.ui.cleanup.CleanUpRequirements; +import org.eclipse.jdt.ui.cleanup.ICleanUp; +import org.eclipse.jdt.ui.cleanup.ICleanUpFix; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.RefactoringStatusEntry; +import org.eclipse.text.edits.TextEdit; +import org.eclipse.text.edits.UndoEdit; + +/** Clean-up step which calls out to the Eclipse JDT clean-up / import sorter. */ +public class EclipseJdtCleanUpStepImpl extends EclipseJdtCoreManipulation { + + /** + * In case of Eclipse JDT clean-up problems (warnings + errors) + * the clean-up step is skipped if not problems shall not be ignored. + *

+ * Value is either 'true' or 'false' ('false' per default) + *

+ */ + public static final String IGNORE_CLEAN_UP_PROBLEMS = "ignoreCleanUpProblems"; + + private final boolean ignoreCleanUpProblems; + private final IJavaProject jdtConfiguration; + private final CleanUpFactory cleanUpFactory; + + public EclipseJdtCleanUpStepImpl(Properties settings) throws Exception { + jdtConfiguration = createProject(settings); + cleanUpFactory = new CleanUpFactory(settings); + ignoreCleanUpProblems = Boolean.parseBoolean(settings.getProperty(IGNORE_CLEAN_UP_PROBLEMS, "false")); + } + + /** Formats Java raw text. The file-location is used in log messages. */ + public String format(String raw, String fileLocation) throws Exception { + ICompilationUnit compilationUnit = createCompilationUnit(raw, jdtConfiguration); + SpotlessRefactoring refactoring = new SpotlessRefactoring(compilationUnit, ignoreCleanUpProblems); + RefactoringStatus report = refactoring.apply(cleanUpFactory.create()); + Arrays.stream(report.getEntries()).map(entry -> new SpotlessStatus(entry, fileLocation)).forEach(status -> logger.log(status)); + return compilationUnit.getBuffer().getContents(); + } + + /** + * Spotless version of {@code org.eclipse.jdt.internal.corext.fix.CleanUpRefactoring}. + *

+ * Spotless does not request (graphical) user feedback neither does it provide undo-information. + * Since Spotless re-factoring / formatting is applied without any further explanation of the changes (preview, warnings, ...), + * it skips per default steps reporting problems (non-fatal errors or warnings) to ensure that the result is as expected by the user. + * Spotless applies the JDT re-factoring without providing a project scope (dependencies, ...). + * Hence steps can cause (fatal) errors which would pass within an Eclipse project. + * Unlike the Eclipse re-factoring process, Spotless does not abort in case a step + * fails, but just reports and skips the step. + */ + private static class SpotlessRefactoring { + + private final ICompilationUnit source; + private final ICompilationUnit[] sources; + private final boolean ignoreProblems; + private final IProgressMonitor doNotMonitor; + private CompilationUnit lazyAst; + private boolean astIsFresh; + + SpotlessRefactoring(ICompilationUnit sourceToRefactor, boolean ignoreCleanUpProblems) { + source = sourceToRefactor; + sources = new ICompilationUnit[]{sourceToRefactor}; + ignoreProblems = ignoreCleanUpProblems; + doNotMonitor = new NullProgressMonitor(); + lazyAst = null; + astIsFresh = false; + } + + RefactoringStatus apply(List steps) throws CoreException { + RefactoringStatus overallStatus = new RefactoringStatus(); + for (ICleanUp step : steps) { + apply(step, overallStatus); + } + return overallStatus; + } + + private void apply(ICleanUp step, RefactoringStatus overallStatus) throws CoreException { + RefactoringStatus preCheckStatus = step.checkPreConditions(source.getJavaProject(), sources, doNotMonitor); + overallStatus.merge(preCheckStatus); + if (isStepOk(preCheckStatus)) { + CleanUpContext context = createContext(step.getRequirements()); + ICleanUpFix fix = step.createFix(context); + RefactoringStatus postCheckStatus = apply(step, Optional.ofNullable(fix)); + overallStatus.merge(postCheckStatus); + } + } + + private RefactoringStatus apply(ICleanUp step, Optional fix) throws CoreException { + RefactoringStatus postCheckStatus = new RefactoringStatus(); + if (fix.isPresent()) { + CompilationUnitChange change = fix.get().createChange(doNotMonitor); + TextEdit edit = change.getEdit(); + if (null != edit) { + UndoEdit undo = source.applyTextEdit(edit, doNotMonitor); + postCheckStatus = step.checkPostConditions(doNotMonitor); + if (isStepOk(postCheckStatus)) { + astIsFresh = false; + } else { + postCheckStatus.addInfo("Undo step " + step.getClass().getSimpleName()); + if (null != undo) { + source.applyTextEdit(undo, doNotMonitor); + } + } + } + } + return postCheckStatus; + } + + private boolean isStepOk(RefactoringStatus stepStatus) { + if (ignoreProblems) { + return stepStatus.getSeverity() < RefactoringStatus.FATAL; + } + return stepStatus.getSeverity() < RefactoringStatus.WARNING; + } + + private CleanUpContext createContext(CleanUpRequirements requirements) { + if ((requirements.requiresAST() && null == lazyAst) || + (requirements.requiresFreshAST() && false == astIsFresh)) { + lazyAst = SharedASTProviderCore.getAST(source, SharedASTProviderCore.WAIT_YES, null); + astIsFresh = true; + } + return new CleanUpContext(source, lazyAst); + } + + }; + + private static class SpotlessStatus implements IStatus { + private final IStatus cleanUpStatus; + private final String fileLocationAsPluginId; + + SpotlessStatus(RefactoringStatusEntry entry, String fileLocation) { + cleanUpStatus = entry.toStatus(); + fileLocationAsPluginId = fileLocation; + } + + @Override + public IStatus[] getChildren() { + return cleanUpStatus.getChildren(); + } + + @Override + public int getCode() { + return cleanUpStatus.getCode(); + } + + @Override + public Throwable getException() { + return cleanUpStatus.getException(); + } + + @Override + public String getMessage() { + return cleanUpStatus.getMessage(); + } + + @Override + public String getPlugin() { + /* + * The plugin ID of the JDT Clean-Up is always a common string. + * Hence it does not add any valuable information for the Spotless user. + * It is replaced by the file location which is hidden from the JDT re-factoring + * process. + */ + return fileLocationAsPluginId; + } + + @Override + public int getSeverity() { + return cleanUpStatus.getSeverity(); + } + + @Override + public boolean isMultiStatus() { + return cleanUpStatus.isMultiStatus(); + } + + @Override + public boolean isOK() { + return cleanUpStatus.isOK(); + } + + @Override + public boolean matches(int severityMask) { + return cleanUpStatus.matches(severityMask); + } + + }; +} diff --git a/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtCoreManipulation.java b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtCoreManipulation.java new file mode 100644 index 0000000000..ec009b5f75 --- /dev/null +++ b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtCoreManipulation.java @@ -0,0 +1,258 @@ +/* + * Copyright 2016 DiffPlug + * + * 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.diffplug.spotless.extra.glue.jdt; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.core.internal.runtime.InternalPlatform; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.ProjectScope; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.ILog; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.content.IContentTypeManager; +import org.eclipse.core.runtime.preferences.DefaultScope; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.jdt.core.IBuffer; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragment; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.manipulation.CodeStyleConfiguration; +import org.eclipse.jdt.core.manipulation.JavaManipulation; +import org.eclipse.jdt.internal.core.BufferManager; +import org.eclipse.jdt.internal.core.CompilationUnit; +import org.eclipse.jdt.internal.core.DefaultWorkingCopyOwner; +import org.eclipse.jdt.internal.core.JavaCorePreferenceInitializer; +import org.eclipse.jdt.internal.core.PackageFragment; +import org.eclipse.jdt.internal.core.nd.indexer.Indexer; +import org.eclipse.jdt.internal.corext.codemanipulation.CodeGenerationSettingsConstants; + +import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework; + +/** + * Basic set-up for formatting features based on {@code org.eclipse.jdt:org.eclipse.jdt.core.manipulation}. + */ +class EclipseJdtCoreManipulation { + // The JDT UI shall be used for creating the settings (JavaUI not imported due to dependencies). + private final static String JDT_UI_PLUGIN_ID = "org.eclipse.jdt.ui"; + + private final static String ROOT_AS_SRC = ""; + private final static String PROJECT_NAME = "spotless"; + private final static String SOURCE_NAME = "source.java"; + + protected final ILog logger; + + private final AtomicInteger uniqueProjectId = new AtomicInteger(0); + private final Map defaultOptions; + + protected EclipseJdtCoreManipulation() throws Exception { + if (SpotlessEclipseFramework.setup( + core -> { + /* + * Indexer needs to exist (but is not used) for JDT clean-up. + * The indexer is not created in headless mode by JDT. + * 'Active' platform state signals non-headless mode ('Resolved' is default state).. + */ + core.add(new org.eclipse.core.internal.registry.osgi.Activator()); + + core.add(new org.eclipse.core.internal.runtime.PlatformActivator()); + core.add(new org.eclipse.core.internal.preferences.Activator()); + core.add(new org.eclipse.core.internal.runtime.Activator()); + }, + config -> { + config.hideEnvironment(); + config.disableDebugging(); + config.ignoreUnsupportedPreferences(); + config.useTemporaryLocations(); + config.changeSystemLineSeparator(); + + /* + * The default 'no content type specific handling' is insufficient. + * The Java source type needs to be recognized by file extension. + */ + config.add(IContentTypeManager.class, new JavaContentTypeManager()); + + config.useSlf4J(EclipseJdtOrganizeImportStepImpl.class.getPackage().getName()); + config.set(InternalPlatform.PROP_OS, ""); //Required for org.eclipse.core.internal.resources.OS initialization + }, + plugins -> { + plugins.applyDefault(); + + //JDT configuration requires an existing project source folder. + plugins.add(new org.eclipse.core.internal.filesystem.Activator()); + plugins.add(new JavaCore()); + })) { + + initializeJdtUiPreferenceDefaults(); + /* + * Assure that the 'allowed keys' are initialized, otherwise + * JProject will not accept any options. + */ + new JavaCorePreferenceInitializer().initializeDefaultPreferences(); + + /* + * Don't run indexer in background (does not disable thread but the job scheduling) + */ + Indexer.getInstance().enableAutomaticIndexing(false); + } + + defaultOptions = new HashMap<>(); + defaultOptions.put(JavaCore.COMPILER_SOURCE, getJavaCoreVersion()); + logger = JavaCore.getPlugin().getLog(); + } + + private static void initializeJdtUiPreferenceDefaults() { + //Following values correspond org.eclipse.jdt.ui.PreferenceConstants (not used due to SWT dependency) + JavaManipulation.setPreferenceNodeId(JDT_UI_PLUGIN_ID); + IEclipsePreferences prefs = DefaultScope.INSTANCE.getNode(JDT_UI_PLUGIN_ID); + + prefs.put(CodeStyleConfiguration.ORGIMPORTS_IMPORTORDER, "java;javax;org;com"); + prefs.put(CodeStyleConfiguration.ORGIMPORTS_ONDEMANDTHRESHOLD, "99"); + prefs.put(CodeStyleConfiguration.ORGIMPORTS_STATIC_ONDEMANDTHRESHOLD, "99"); + + prefs.put(CodeGenerationSettingsConstants.CODEGEN_KEYWORD_THIS, "false"); + prefs.put(CodeGenerationSettingsConstants.CODEGEN_USE_OVERRIDE_ANNOTATION, "false"); + prefs.put(CodeGenerationSettingsConstants.CODEGEN_ADD_COMMENTS, "true"); + prefs.put(CodeGenerationSettingsConstants.ORGIMPORTS_IGNORELOWERCASE, "true"); + } + + private static String getJavaCoreVersion() { + final String javaVersion = System.getProperty("java.version"); + final List orderedSupportedCoreVersions = JavaCore.getAllVersions(); + for (String coreVersion : orderedSupportedCoreVersions) { + if (javaVersion.startsWith(coreVersion)) { + return coreVersion; + } + } + return orderedSupportedCoreVersions.get(orderedSupportedCoreVersions.size() - 1); + } + + /** + * Creates a JAVA project and applies the configuration. + * @param settings Configuration settings + * @return Configured JAVA project + * @throws Exception In case the project creation fails + */ + protected final IJavaProject createProject(Properties settings) throws Exception { + String uniqueProjectName = String.format("%s-%d", PROJECT_NAME, uniqueProjectId.incrementAndGet()); + IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(uniqueProjectName); + // The project must be open before items (natures, folders, sources, ...) can be created + if (!project.exists()) { + project.create(null); //Might still exist in case of restarts and dedicated class loader + } + project.open(0, null); + + //If the project nature is not set, the AST is not created for the compilation units + IProjectDescription description = project.getDescription(); + description.setNatureIds(new String[]{JavaCore.NATURE_ID}); + project.setDescription(description, null); + IJavaProject jProject = JavaCore.create(project); + + Map allSettings = new HashMap<>(defaultOptions); + settings.forEach((key, value) -> { + allSettings.put(key.toString(), value.toString()); + }); + //Configure JDT manipulation processor + IEclipsePreferences projectPrefs = new ProjectScope(project.getProject()).getNode(JavaManipulation.getPreferenceNodeId()); + allSettings.forEach((key, value) -> { + projectPrefs.put(key.toString(), value.toString()); + }); + /* + * Configure options taken directly from the Java project (without qualifier). + * Whether a setting is a Java project option or not, is filtered by the + * JavaCorePreferenceInitializer, initialized by the constructor of this class. + */ + jProject.setOptions(allSettings); + + // Eclipse source files require an existing source folder for creation + IPackageFragmentRoot src = jProject.getPackageFragmentRoot(jProject.getProject()); + IPackageFragment pkg = src.createPackageFragment(ROOT_AS_SRC, true, null); + IFolder folder = project.getFolder(uniqueProjectName); + if (!folder.exists()) { + folder.create(0, false, null); + } + + // Eclipse clean-up requires an existing source file + pkg.createCompilationUnit(SOURCE_NAME, "", true, null); + + return jProject; + } + + protected final ICompilationUnit createCompilationUnit(String contents, IJavaProject jProject) throws Exception { + IPackageFragmentRoot src = jProject.getPackageFragmentRoot(jProject.getProject()); + IPackageFragment pkg = src.getPackageFragment(ROOT_AS_SRC); + return new RamCompilationUnit((PackageFragment) pkg, contents); + } + + /** Keep compilation units in RAM */ + private static class RamCompilationUnit extends CompilationUnit { + + /* + * Each RAM compilation unit has its own buffer manager to + * prevent dropping of CUs when a maximum size is reached. + */ + private final RamBufferManager manager; + + RamCompilationUnit(PackageFragment parent, String contents) { + super(parent, SOURCE_NAME, DefaultWorkingCopyOwner.PRIMARY); + manager = new RamBufferManager(); + IBuffer buffer = BufferManager.createBuffer(this); + buffer.setContents(contents.toCharArray()); + manager.add(buffer); + } + + @Override + public boolean exists() { + return true; + } + + @Override + protected BufferManager getBufferManager() { + return manager; + } + + @Override + public void save(IProgressMonitor pm, boolean force) throws JavaModelException { + //RAM CU is never stored on disk + } + + @Override + public ICompilationUnit getWorkingCopy(IProgressMonitor monitor) throws JavaModelException { + return new RamCompilationUnit((PackageFragment) this.getParent(), getBuffer().getContents()); + } + + @Override + public boolean equals(Object obj) { + return this == obj; //Working copies are not supported + } + } + + /** Work around package privileges */ + private static class RamBufferManager extends BufferManager { + void add(IBuffer buffer) { + addBuffer(buffer); + } + } +} diff --git a/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtOrganizeImportStepImpl.java b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtOrganizeImportStepImpl.java new file mode 100644 index 0000000000..910cfe6b89 --- /dev/null +++ b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtOrganizeImportStepImpl.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016 DiffPlug + * + * 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.diffplug.spotless.extra.glue.jdt; + +import java.util.Properties; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.manipulation.OrganizeImportsOperation; +import org.eclipse.jdt.core.manipulation.SharedASTProviderCore; + +/** Clean-up step which calls out to the Eclipse JDT clean-up / import sorter. */ +public class EclipseJdtOrganizeImportStepImpl extends EclipseJdtCoreManipulation { + private final IJavaProject jdtConfiguration; //The project stores the JDT clean-up configuration + + public EclipseJdtOrganizeImportStepImpl(Properties settings) throws Exception { + jdtConfiguration = createProject(settings); + } + + public String format(String raw) throws Exception { + ICompilationUnit compilationUnit = createCompilationUnit(raw, jdtConfiguration); + CompilationUnit ast = SharedASTProviderCore.getAST(compilationUnit, SharedASTProviderCore.WAIT_YES, null); + OrganizeImportsOperation formatOperation = new OrganizeImportsOperation(compilationUnit, ast, false, false, true, null); + try { + formatOperation.run(null); + return compilationUnit.getSource(); + } catch (OperationCanceledException | CoreException e) { + throw new IllegalArgumentException("Invalid java syntax for formatting.", e); + } + } +} diff --git a/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/JavaContentTypeManager.java b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/JavaContentTypeManager.java new file mode 100644 index 0000000000..2ddf5d4d92 --- /dev/null +++ b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/JavaContentTypeManager.java @@ -0,0 +1,67 @@ +/* + * Copyright 2016 DiffPlug + * + * 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.diffplug.spotless.extra.glue.jdt; + +import org.eclipse.core.internal.content.ContentType; +import org.eclipse.core.internal.content.ContentTypeCatalog; +import org.eclipse.core.runtime.content.IContentType; +import org.eclipse.core.runtime.content.IContentTypeMatcher; +import org.eclipse.core.runtime.preferences.IScopeContext; +import org.eclipse.jdt.internal.compiler.util.SuffixConstants; + +import com.diffplug.spotless.extra.eclipse.base.service.NoContentTypeSpecificHandling; + +/** + * Java compilation unit validation requires Java content type to be recognized. + * All source is assumed to be Java. A content description is not required/provided. + *

+ * See {@code org.eclipse.jdt.internal.core.util.Util} for details. + *

+ */ +public class JavaContentTypeManager extends NoContentTypeSpecificHandling { + + private final IContentType contentType; + + public JavaContentTypeManager() { + contentType = ContentType.createContentType( + new ContentTypeCatalog(null, 0), + "", + "", + (byte) 0, + new String[]{SuffixConstants.EXTENSION_java, SuffixConstants.EXTENSION_JAVA}, + new String[0], + new String[0], + "", + "", + null, + null); + } + + @Override + public IContentType getContentType(String contentTypeIdentifier) { + return contentType; + } + + @Override + public IContentType[] getAllContentTypes() { + return new IContentType[]{contentType}; + } + + @Override + public IContentTypeMatcher getMatcher(ISelectionPolicy customPolicy, IScopeContext context) { + return this; + } +} diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtCleanUpStep.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtCleanUpStep.java new file mode 100644 index 0000000000..95fb774676 --- /dev/null +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtCleanUpStep.java @@ -0,0 +1,70 @@ +/* + * Copyright 2016-2023 DiffPlug + * + * 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.diffplug.spotless.extra.java; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.Jvm; +import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.extra.EquoBasedStepBuilder; + +import dev.equo.solstice.p2.P2Model; + +import java.io.File; +import java.util.Properties; + +/** Formatter step which calls out to the Eclipse JDT formatter. */ +public final class EclipseJdtCleanUpStep { + // prevent direct instantiation + private EclipseJdtCleanUpStep() {} + + private static final String NAME = "eclipse jdt cleanup"; + private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(11, "4.26"); + + public static String defaultVersion() { + return JVM_SUPPORT.getRecommendedFormatterVersion(); + } + + public static EquoBasedStepBuilder createBuilder(Provisioner provisioner) { + return new EquoBasedStepBuilder(NAME, provisioner, EclipseJdtCleanUpStep::apply) { + @Override + protected P2Model model(String version) { + var model = new P2Model(); + addPlatformRepo(model, version); + model.getInstall().add("org.eclipse.jdt.core"); + return model; + } + + @Override + public void setVersion(String version) { + if (version.endsWith(".0")) { + String newVersion = version.substring(0, version.length() - 2); + System.err.println("Recommend replacing '" + version + "' with '" + newVersion + "' for Eclipse JDT"); + version = newVersion; + } + super.setVersion(version); + } + }; + } + + private static FormatterFunc apply(EquoBasedStepBuilder.State state) throws Exception { + JVM_SUPPORT.assertFormatterSupported(state.getSemanticVersion()); + Class formatterClazz = state.getJarState().getClassLoader().loadClass("com.diffplug.spotless.extra.glue.jdt.EclipseJdtCleanUpStepImpl"); + var formatter = formatterClazz.getConstructor(Properties.class).newInstance(state.getPreferences()); + var method = formatterClazz.getMethod("format", String.class, File.class); + FormatterFunc formatterFunc = (FormatterFunc.NeedsFile) (input, file) -> (String) method.invoke(formatter, input, file); + return JVM_SUPPORT.suggestLaterVersionOnError(state.getSemanticVersion(), formatterFunc); + } +} diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtOrganizeImportsStep.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtOrganizeImportsStep.java new file mode 100644 index 0000000000..ed446f5378 --- /dev/null +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtOrganizeImportsStep.java @@ -0,0 +1,70 @@ +/* + * Copyright 2016-2023 DiffPlug + * + * 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.diffplug.spotless.extra.java; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.Jvm; +import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.extra.EquoBasedStepBuilder; + +import dev.equo.solstice.p2.P2Model; + +import java.io.File; +import java.util.Properties; + +/** Formatter step which calls out to the Eclipse JDT formatter. */ +public final class EclipseJdtOrganizeImportsStep { + // prevent direct instantiation + private EclipseJdtOrganizeImportsStep() {} + + private static final String NAME = "eclipse jdt organizeImports"; + private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(11, "4.26"); + + public static String defaultVersion() { + return JVM_SUPPORT.getRecommendedFormatterVersion(); + } + + public static EquoBasedStepBuilder createBuilder(Provisioner provisioner) { + return new EquoBasedStepBuilder(NAME, provisioner, EclipseJdtOrganizeImportsStep::apply) { + @Override + protected P2Model model(String version) { + var model = new P2Model(); + addPlatformRepo(model, version); + model.getInstall().add("org.eclipse.jdt.core"); + return model; + } + + @Override + public void setVersion(String version) { + if (version.endsWith(".0")) { + String newVersion = version.substring(0, version.length() - 2); + System.err.println("Recommend replacing '" + version + "' with '" + newVersion + "' for Eclipse JDT"); + version = newVersion; + } + super.setVersion(version); + } + }; + } + + private static FormatterFunc apply(EquoBasedStepBuilder.State state) throws Exception { + JVM_SUPPORT.assertFormatterSupported(state.getSemanticVersion()); + Class formatterClazz = state.getJarState().getClassLoader().loadClass("com.diffplug.spotless.extra.glue.jdt.EclipseJdtOrganizeImportsStepImpl"); + var formatter = formatterClazz.getConstructor(Properties.class).newInstance(state.getPreferences()); + var method = formatterClazz.getMethod("format", String.class, File.class); + FormatterFunc formatterFunc = (FormatterFunc.NeedsFile) (input, file) -> (String) method.invoke(formatter, input, file); + return JVM_SUPPORT.suggestLaterVersionOnError(state.getSemanticVersion(), formatterFunc); + } +} diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtCleanUpStepImplTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtCleanUpStepImplTest.java new file mode 100644 index 0000000000..7805537a6c --- /dev/null +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtCleanUpStepImplTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2016 DiffPlug + * + * 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.diffplug.spotless.extra.java; + +import java.util.Properties; +import java.util.function.Consumer; + +import com.diffplug.spotless.TestProvisioner; +import com.diffplug.spotless.extra.EquoBasedStepBuilder; +import com.diffplug.spotless.extra.eclipse.EquoResourceHarness; + +import org.assertj.core.util.Arrays; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.manipulation.CodeStyleConfiguration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; + +/** Eclipse JDT wrapper integration tests */ +public class EclipseJdtCleanUpStepImplTest extends EquoResourceHarness { + private final static String INPUT = "package p; class C{}"; + private final static String EXPECTED = "package p;\nclass C {\n}"; + private static String SOURCE_FILE_PATH = "some .. / \\ ill&formatted/$path"; + private static TestData TEST_DATA = null; + + @BeforeAll + public static void initializeStatic() throws Exception { + TEST_DATA = TestData.getTestDataOnFileSystem(); + } + + private static EquoBasedStepBuilder createBuilder() { + return EclipseJdtCleanUpStep.createBuilder(TestProvisioner.mavenCentral()); + } + + public EclipseJdtCleanUpStepImplTest() { + super(createBuilder(), INPUT, EXPECTED); + } + + @Test + public void emptyInput() throws Throwable { + cleanUpTest("", "", config -> {}); + } + + @Test + public void defaultConfiguration() throws Throwable { + for (String testFile : Arrays.array("Simple", "Statics", "Wildcards")) { + try { + cleanUpTest(testFile, config -> {}); + } catch (AssertionFailedError e) { + throw new AssertionFailedError(testFile + " - " + e.getMessage(), e.getExpected(), e.getActual()); + } + } + } + + @Test + public void invalidConfiguration() throws Throwable { + //Smoke test, no exceptions expected + cleanUpTest("", "", config -> { + config.put("invalid.key", "some.value"); + }); + cleanUpTest("", "", config -> { + config.put(JavaCore.COMPILER_SOURCE, "-42"); + }); + cleanUpTest("", "", config -> { + config.put(JavaCore.COMPILER_SOURCE, "Not an integer"); + }); + } + + @Test + public void importConfiguration() throws Throwable { + String defaultOrganizedInput = TEST_DATA.input("ImportConfiguration"); + cleanUpTest(defaultOrganizedInput, defaultOrganizedInput, config -> {}); + + String customOrganizedOutput = TEST_DATA.afterOrganizedImports("ImportConfiguration"); + cleanUpTest(defaultOrganizedInput, customOrganizedOutput, config -> { + config.put(CodeStyleConfiguration.ORGIMPORTS_IMPORTORDER, "foo;#foo;"); + }); + } + + private static void cleanUpTest(final String fileName, final Consumer config) throws Exception { + cleanUpTest(TEST_DATA.input(fileName), TEST_DATA.afterCleanUp(fileName), config); + } + + private static void cleanUpTest(final String input, final String expected, final Consumer config) throws Exception { + Properties properties = new Properties(); + config.accept(properties); + EclipseJdtCleanUpStepImpl formatter = new EclipseJdtCleanUpStepImpl(properties); + String output = formatter.format(input, SOURCE_FILE_PATH); + Assertions.assertEquals("Unexpected clean-up result " + TestData.toString(properties), + expected, output); + } +} diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtOrganizeImportStepImplTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtOrganizeImportStepImplTest.java new file mode 100644 index 0000000000..8b23adf7ce --- /dev/null +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtOrganizeImportStepImplTest.java @@ -0,0 +1,113 @@ +/* + * Copyright 2016 DiffPlug + * + * 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.diffplug.spotless.extra.java; + +import java.util.Properties; +import java.util.function.Consumer; + +import com.diffplug.spotless.TestProvisioner; +import com.diffplug.spotless.extra.EquoBasedStepBuilder; +import com.diffplug.spotless.extra.eclipse.EquoResourceHarness; + +import org.assertj.core.util.Arrays; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.manipulation.CodeStyleConfiguration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; + +/** Eclipse JDT wrapper integration tests */ +public class EclipseJdtOrganizeImportStepImplTest extends EquoResourceHarness { + private final static String INPUT = "package p; class C{}"; + private final static String EXPECTED = "package p;\nclass C {\n}"; + + private static TestData TEST_DATA = null; + + @BeforeAll + public static void initializeStatic() throws Exception { + TEST_DATA = TestData.getTestDataOnFileSystem(); + } + + private static EquoBasedStepBuilder createBuilder() { + return EclipseJdtOrganizeImportStep.createBuilder(TestProvisioner.mavenCentral()); + } + + public EclipseJdtOrganizeImportStepImplTest() { + super(createBuilder(), INPUT, EXPECTED); + } + + @Test + public void emptyInput() throws Throwable { + organizeImportTest("", "", config -> {}); + } + + @Test + public void defaultConfiguration() throws Throwable { + for (String testFile : Arrays.array("Simple", "Statics", "Wildcards")) { + try { + organizeImportTest(testFile, config -> {}); + } catch (AssertionFailedError e) { + throw new AssertionFailedError(testFile + " - " + e.getMessage(), e.getExpected(), e.getActual()); + } + } + } + + @Test + public void defaultPackage() throws Throwable { + String input = TEST_DATA.input("Simple").replaceFirst("package .+", ""); + String expected = TEST_DATA.afterOrganizedImports("Simple").replaceFirst("package .+", ""); + organizeImportTest(input, expected, config -> {}); + } + + @Test + public void invalidConfiguration() throws Throwable { + //Smoke test, no exceptions expected + organizeImportTest("", "", config -> { + config.put("invalid.key", "some.value"); + }); + organizeImportTest("", "", config -> { + config.put(JavaCore.COMPILER_SOURCE, "-42"); + }); + organizeImportTest("", "", config -> { + config.put(JavaCore.COMPILER_SOURCE, "Not an integer"); + }); + } + + @Test + public void customConfiguration() throws Throwable { + String defaultOrganizedInput = TEST_DATA.input("ImportConfiguration"); + organizeImportTest(defaultOrganizedInput, defaultOrganizedInput, config -> {}); + + String customOrganizedOutput = TEST_DATA.afterOrganizedImports("ImportConfiguration"); + organizeImportTest(defaultOrganizedInput, customOrganizedOutput, config -> { + config.put(CodeStyleConfiguration.ORGIMPORTS_IMPORTORDER, "foo;#foo;"); + }); + } + + private static void organizeImportTest(final String fileName, final Consumer config) throws Exception { + organizeImportTest(TEST_DATA.input(fileName), TEST_DATA.afterOrganizedImports(fileName), config); + } + + private static void organizeImportTest(final String input, final String expected, final Consumer config) throws Exception { + Properties properties = new Properties(); + config.accept(properties); + EclipseJdtOrganizeImportStepImpl formatter = new EclipseJdtOrganizeImportStepImpl(properties); + String output = formatter.format(input); + Assertions.assertEquals("Unexpected import organization " + TestData.toString(properties), + expected, output); + } +} diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/TestData.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/TestData.java new file mode 100644 index 0000000000..db6aaa5b72 --- /dev/null +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/TestData.java @@ -0,0 +1,101 @@ +/* + * Copyright 2016 DiffPlug + * + * 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.diffplug.spotless.extra.java; +/* + * Copyright 2016 DiffPlug + * + * 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. + */ + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Properties; + +public class TestData { + private static final String EXTENSION_INPUT = ".input"; + private static final String EXTENSION_ORGANIZED_IMPORTS = ".organized"; + private static final String EXTENSION_CLEAN_UP = ".cleanup"; + + public static TestData getTestDataOnFileSystem() { + final String userDir = System.getProperty("user.dir", "."); + Path dataPath = Paths.get(userDir, "src", "test", "resources"); + if (Files.isDirectory(dataPath)) { + return new TestData(dataPath); + } + throw new IllegalArgumentException("Test data not found:" + dataPath.toString()); + } + + private final Path resourcesPath; + + private TestData(Path resourcesPath) { + this.resourcesPath = resourcesPath.toAbsolutePath(); + if (!Files.isDirectory(resourcesPath)) { + throw new IllegalArgumentException(String.format("'%1$s' is not a directory.", resourcesPath)); + } + } + + public String input(final String fileName) throws Exception { + Path filePath = resourcesPath.resolve(fileName + EXTENSION_INPUT); + return read(filePath); + } + + public String afterOrganizedImports(final String fileName) { + Path filePath = resourcesPath.resolve(fileName + EXTENSION_ORGANIZED_IMPORTS); + return read(filePath); + } + + public String afterCleanUp(final String fileName) { + Path filePath = resourcesPath.resolve(fileName + EXTENSION_CLEAN_UP); + return read(filePath); + } + + private String read(final Path filePath) { + if (!Files.isRegularFile(filePath)) { + throw new IllegalArgumentException(String.format("'%1$s' is not a regular file.", filePath)); + } + try { + String checkedOutFileContent = new String(java.nio.file.Files.readAllBytes(filePath), "ASCII"); + return checkedOutFileContent.replace("\r", ""); //Align GIT end-of-line normalization + } catch (IOException e) { + throw new IllegalArgumentException(String.format("Failed to read '%1$s'.", filePath), e); + } + } + + public static String toString(Properties properties) { + StringBuilder result = new StringBuilder(); + result.append('['); + properties.forEach((k, v) -> { + result.append(k.toString()); + result.append('='); + result.append(v.toString()); + result.append(';'); + }); + result.append(']'); + return result.toString(); + } +} diff --git a/testlib/src/main/resources/java/eclipse/cleanup/ImportConfiguration.input b/testlib/src/main/resources/java/eclipse/cleanup/ImportConfiguration.input new file mode 100644 index 0000000000..ba163e17f2 --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/cleanup/ImportConfiguration.input @@ -0,0 +1,12 @@ +package bar.foo; + +import static foo.bar.B.someMethod; //Default configuration places static imports before none-static + +import foo.bar.B; + +class A { + static { + someMethod(); + new B(); + } +} diff --git a/testlib/src/main/resources/java/eclipse/cleanup/ImportConfiguration.organized b/testlib/src/main/resources/java/eclipse/cleanup/ImportConfiguration.organized new file mode 100644 index 0000000000..e51cf93fd7 --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/cleanup/ImportConfiguration.organized @@ -0,0 +1,12 @@ +package bar.foo; + +import foo.bar.B; + +import static foo.bar.B.someMethod; //Default configuration places static imports before none-static + +class A { + static { + someMethod(); + new B(); + } +} diff --git a/testlib/src/main/resources/java/eclipse/cleanup/Simple.input b/testlib/src/main/resources/java/eclipse/cleanup/Simple.input new file mode 100644 index 0000000000..a837ae3d8d --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/cleanup/Simple.input @@ -0,0 +1,25 @@ +package bar.foo; + +import javax.net.SocketFactory; //Wrong order in Other group + +import java.net.Socket; //Wrong within group +import java.lang.System; //Can be removed since implicitly imported +import java.io.IOException; +import java.io.PrintStream; //Class can be found but is not used + +import foo.bar.C; //Class cannot be found and is not used +import foo.bar.B; //Class cannot be found but is used + + +class A { + private static final SocketFactory FACTORY; + static { + FACTORY = SocketFactory.getDefault(); + } + public static Socket open() throws IOException { + return FACTORY.createSocket(); + } + public static B create() { + return new B(); + } +} diff --git a/testlib/src/main/resources/java/eclipse/cleanup/Simple.organized b/testlib/src/main/resources/java/eclipse/cleanup/Simple.organized new file mode 100644 index 0000000000..cead7fd671 --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/cleanup/Simple.organized @@ -0,0 +1,22 @@ +package bar.foo; + +import java.io.IOException; +import java.net.Socket; //Wrong within group + +import javax.net.SocketFactory; //Wrong order in Other group + +import foo.bar.B; //Class cannot be found but is used + + +class A { + private static final SocketFactory FACTORY; + static { + FACTORY = SocketFactory.getDefault(); + } + public static Socket open() throws IOException { + return FACTORY.createSocket(); + } + public static B create() { + return new B(); + } +} diff --git a/testlib/src/main/resources/java/eclipse/cleanup/Statics.input b/testlib/src/main/resources/java/eclipse/cleanup/Statics.input new file mode 100644 index 0000000000..fb7e77809f --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/cleanup/Statics.input @@ -0,0 +1,15 @@ +package bar.foo; + +import static foo.bar.B.someMethod; // Is used +import static foo.bar.B.someOtherMethod; // Covered already by non-static import +import static foo.bar.B.notUsed; // Is not used + +import foo.bar.B; + +class A { + static { + someMethod(); + B.someOtherMethod(); + new B(); + } +} diff --git a/testlib/src/main/resources/java/eclipse/cleanup/Statics.organized b/testlib/src/main/resources/java/eclipse/cleanup/Statics.organized new file mode 100644 index 0000000000..d88b552ff6 --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/cleanup/Statics.organized @@ -0,0 +1,13 @@ +package bar.foo; + +import static foo.bar.B.someMethod; // Is used + +import foo.bar.B; + +class A { + static { + someMethod(); + B.someOtherMethod(); + new B(); + } +} diff --git a/testlib/src/main/resources/java/eclipse/cleanup/Wildcards.input b/testlib/src/main/resources/java/eclipse/cleanup/Wildcards.input new file mode 100644 index 0000000000..3576db1b61 --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/cleanup/Wildcards.input @@ -0,0 +1,18 @@ +package bar.foo; + +import static foo.bar.C.*; //Might be used +import static foo.bar.B.someMethod; //Is used +import static foo.bar.B.someOtherMethod; //Is not used +import foo.bar.B; //Is used +import foo.bar.E; //Not used +import hello.world.*; //Might be used + +class A { + public static B create() { + someMethodOfC(); + new ClassInHelloWorld(); + + someMethod(); + return new B(); + } +} diff --git a/testlib/src/main/resources/java/eclipse/cleanup/Wildcards.organized b/testlib/src/main/resources/java/eclipse/cleanup/Wildcards.organized new file mode 100644 index 0000000000..8f7b8ef389 --- /dev/null +++ b/testlib/src/main/resources/java/eclipse/cleanup/Wildcards.organized @@ -0,0 +1,17 @@ +package bar.foo; + +import static foo.bar.B.someMethod; //Is used +import static foo.bar.C.*; //Might be used + +import foo.bar.B; //Is used +import hello.world.*; //Might be used + +class A { + public static B create() { + someMethodOfC(); + new ClassInHelloWorld(); + + someMethod(); + return new B(); + } +}