diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/rewrite/describing/ImportRewriteTest.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/rewrite/describing/ImportRewriteTest.java index 1f4dcb4..ce51503 100644 --- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/rewrite/describing/ImportRewriteTest.java +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/rewrite/describing/ImportRewriteTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2014 IBM Corporation and others. + * Copyright (c) 2000, 2015 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -8,14 +8,21 @@ * Contributors: * IBM Corporation - initial API and implementation * Stephan Herrmann - Contribution for Bug 378024 - Ordering of comments between imports not preserved + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 *******************************************************************************/ package org.eclipse.jdt.core.tests.rewrite.describing; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; import junit.framework.Test; +import org.eclipse.core.resources.ProjectScope; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.Platform; import org.eclipse.jdt.core.BindingKey; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaElement; @@ -32,11 +39,13 @@ import org.eclipse.jdt.core.dom.Type; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; +import org.eclipse.jdt.core.dom.rewrite.ImportRewrite.ImportRewriteContext; import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants; import org.eclipse.jdt.core.tests.model.AbstractJavaModelTests; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.Document; import org.eclipse.text.edits.MalformedTreeException; +import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.TextEdit; import org.osgi.service.prefs.BackingStoreException; @@ -83,6 +92,8 @@ proj.setOption(DefaultCodeFormatterConstants.FORMATTER_BLANK_LINES_BETWEEN_IMPORT_GROUPS, String.valueOf(1)); + // The tests in this class assume that the line separator is "\n". + new ProjectScope(proj.getProject()).getNode(Platform.PI_RUNTIME).put(Platform.PREF_LINE_SEPARATOR, "\n"); this.sourceFolder = getPackageFragmentRoot("P", "src"); @@ -94,6 +105,1310 @@ super.tearDown(); } + /** + * Addresses https://bugs.eclipse.org/412929 ("Adding a type results in adding a package and + * later does not honor order"). + */ + public void testImportGroupMatchingQualifiedName() throws Exception { + ICompilationUnit cu = createCompilationUnit("pack1", "C"); + + String[] order = new String[] { "#android.R.doFoo", "android.R", "java", "android" }; + + ImportRewrite imports = newImportsRewrite(cu, order, 999, 999, false); + imports.setUseContextToFilterImplicitImports(true); + imports.addImport("android.R"); + imports.addImport("java.util.List"); + imports.addImport("android.Foo"); + imports.addStaticImport("android.R", "doFoo", false); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import static android.R.doFoo;\n"); + expected.append("\n"); + expected.append("import android.R;\n"); + expected.append("\n"); + expected.append("import java.util.List;\n"); + expected.append("\n"); + expected.append("import android.Foo;\n"); + expected.append("\n"); + expected.append("public class C {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Expects that the comments from single imports are reassigned + * to a new on-demand import into which they are reduced. + */ + public void testReduceNewOnDemand() throws Exception { + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("import java.io.Serializable;\n"); + contents.append("\n"); + contents.append("// A floating leading\n"); + contents.append("\n"); + contents.append("// A leading\n"); + contents.append("/* A same-line leading */ import java.net.A; // A same-line trailing\n"); + contents.append("// A trailing\n"); + contents.append("\n"); + contents.append("// B floating leading\n"); + contents.append("\n"); + contents.append("// B leading\n"); + contents.append("/* B same-line leading */ import java.net.B; // B same-line trailing\n"); + contents.append("// B trailing\n"); + contents.append("\n"); + contents.append("// C floating leading\n"); + contents.append("\n"); + contents.append("// C leading\n"); + contents.append("/* C same-line leading */ import java.net.C; // C same-line trailing\n"); + contents.append("// C trailing\n"); + contents.append("\n"); + contents.append("import java.util.List;\n"); + contents.append("\n"); + contents.append("public class Clazz {}"); + ICompilationUnit cu = createCompilationUnit("pack1", "Clazz", contents.toString()); + + String[] order = new String[] { "java" }; + + ImportRewrite imports = newImportsRewrite(cu, order, 1, 1, false); + imports.setUseContextToFilterImplicitImports(true); + imports.addImport("java.io.Serializable"); + imports.addImport("java.net.A"); + imports.addImport("java.net.B"); + imports.addImport("java.net.C"); + imports.addImport("java.util.List"); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import java.io.*;\n"); + expected.append("\n"); + expected.append("// A floating leading\n"); + expected.append("\n"); + expected.append("// A leading\n"); + expected.append("/* A same-line leading */\n"); + expected.append("// A same-line trailing\n"); + expected.append("// A trailing\n"); + expected.append("\n"); + expected.append("// B floating leading\n"); + expected.append("\n"); + expected.append("// B leading\n"); + expected.append("/* B same-line leading */\n"); + expected.append("// B same-line trailing\n"); + expected.append("// B trailing\n"); + expected.append("\n"); + expected.append("// C floating leading\n"); + expected.append("\n"); + expected.append("// C leading\n"); + expected.append("/* C same-line leading */\n"); + expected.append("// C same-line trailing\n"); + expected.append("// C trailing\n"); + expected.append("import java.net.*;\n"); + expected.append("import java.util.*;\n"); + expected.append("\n"); + expected.append("public class Clazz {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Expects that the comments from single imports are reassigned + * to an existing on-demand import into which they are reduced, + * and that the on-demand import's own comments are preserved. + */ + public void testReduceExistingOnDemand() throws Exception { + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("import java.io.*;\n"); + contents.append("\n"); + contents.append("// on-demand floating\n"); + contents.append("\n"); + contents.append("// on-demand leading\n"); + contents.append("/* on-demand same-line leading */ import java.net.*; // on-demand same-line trailing\n"); + contents.append("// on-demand trailing\n"); + contents.append("\n"); + contents.append("// A floating leading\n"); + contents.append("\n"); + contents.append("// A leading\n"); + contents.append("/* A same-line leading */ import java.net.A; // A same-line trailing\n"); + contents.append("// A trailing\n"); + contents.append("\n"); + contents.append("// B floating leading\n"); + contents.append("\n"); + contents.append("// B leading\n"); + contents.append("/* B same-line leading */ import java.net.B; // B same-line trailing\n"); + contents.append("// B trailing\n"); + contents.append("\n"); + contents.append("// C floating leading\n"); + contents.append("\n"); + contents.append("// C leading\n"); + contents.append("/* C same-line leading */ import java.net.C; // C same-line trailing\n"); + contents.append("// C trailing\n"); + contents.append("\n"); + contents.append("import java.util.*;\n"); + contents.append("\n"); + contents.append("public class Clazz {}"); + ICompilationUnit cu = createCompilationUnit("pack1", "Clazz", contents.toString()); + + String[] order = new String[] { "java.io", "java", "java.util" }; + + ImportRewrite imports = newImportsRewrite(cu, order, 1, 1, false); + imports.setUseContextToFilterImplicitImports(true); + imports.addImport("java.io.Serializable"); + imports.addImport("java.net.A"); + imports.addImport("java.net.B"); + imports.addImport("java.net.C"); + imports.addImport("java.util.List"); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import java.io.*;\n"); + expected.append("\n"); + expected.append("// A floating leading\n"); + expected.append("\n"); + expected.append("// A leading\n"); + expected.append("/* A same-line leading */\n"); + expected.append("// A same-line trailing\n"); + expected.append("// A trailing\n"); + expected.append("\n"); + expected.append("// B floating leading\n"); + expected.append("\n"); + expected.append("// B leading\n"); + expected.append("/* B same-line leading */\n"); + expected.append("// B same-line trailing\n"); + expected.append("// B trailing\n"); + expected.append("\n"); + expected.append("// C floating leading\n"); + expected.append("\n"); + expected.append("// C leading\n"); + expected.append("/* C same-line leading */\n"); + expected.append("// C same-line trailing\n"); + expected.append("// C trailing\n"); + expected.append("\n"); + expected.append("// on-demand floating\n"); + expected.append("\n"); + expected.append("// on-demand leading\n"); + expected.append("/* on-demand same-line leading */ import java.net.*; // on-demand same-line trailing\n"); + expected.append("// on-demand trailing\n"); + expected.append("\n"); + expected.append("import java.util.*;\n"); + expected.append("\n"); + expected.append("public class Clazz {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Expects that comments from an expanded on-demand import are reassigned + * to a corresponding single import, and that comments of other single imports + * with the same container name are preserved. + */ + public void testExpandOnDemand() throws Exception { + StringBuffer contents = new StringBuffer(); + contents.append("package pack1\n"); + contents.append("\n"); + contents.append("import com.example;\n"); + contents.append("\n"); + contents.append("/* on-demand floating */\n"); + contents.append("\n"); + contents.append("// on-demand leading\n"); + contents.append("/* on-demand same-line leading */ import java.util.*; // on-demand same-line trailing\n"); + contents.append("// on-demand trailing\n"); + contents.append("\n"); + contents.append("/* ArrayList floating */\n"); + contents.append("\n"); + contents.append("// ArrayList leading\n"); + contents.append("/* ArrayList same-line leading */ import java.util.ArrayList; // ArrayList same-line trailing\n"); + contents.append("// ArrayList trailing\n"); + contents.append("\n"); + contents.append("/* List floating */\n"); + contents.append("\n"); + contents.append("// List leading\n"); + contents.append("/* List same-line leading */ import java.util.List; // List same-line trailing\n"); + contents.append("// List trailing\n"); + contents.append("\n"); + contents.append("/* Map floating */\n"); + contents.append("\n"); + contents.append("// Map leading\n"); + contents.append("/* Map same-line leading */ import java.util.Map; // Map same-line trailing\n"); + contents.append("// Map trailing\n"); + contents.append("\n"); + contents.append("import java.net.Socket;\n"); + contents.append("\n"); + contents.append("public class C {}\n"); + ICompilationUnit cu = createCompilationUnit("pack1", "C", contents.toString()); + + String[] order = new String[] { "com", "java.util", "java.net" }; + + ImportRewrite importRewrite = newImportsRewrite(cu, order, 999, 999, false); + importRewrite.setUseContextToFilterImplicitImports(true); + importRewrite.addImport("java.util.ArrayList"); + importRewrite.addImport("java.util.Map"); + importRewrite.addImport("java.util.Set"); + importRewrite.addImport("java.net.Socket"); + + apply(importRewrite); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1\n"); + expected.append("\n"); + expected.append("/* on-demand floating */\n"); + expected.append("\n"); + expected.append("// on-demand leading\n"); + expected.append("/* on-demand same-line leading */\n"); + expected.append("// on-demand same-line trailing\n"); + expected.append("// on-demand trailing\n"); + expected.append("\n"); + expected.append("/* ArrayList floating */\n"); + expected.append("\n"); + expected.append("// ArrayList leading\n"); + expected.append("/* ArrayList same-line leading */ import java.util.ArrayList; // ArrayList same-line trailing\n"); + expected.append("// ArrayList trailing\n"); + expected.append("\n"); + expected.append("/* Map floating */\n"); + expected.append("\n"); + expected.append("// Map leading\n"); + expected.append("/* Map same-line leading */ import java.util.Map; // Map same-line trailing\n"); + expected.append("// Map trailing\n"); + expected.append("import java.util.Set;\n"); + expected.append("\n"); + expected.append("import java.net.Socket;\n"); + expected.append("\n"); + expected.append("public class C {}\n"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Expects that the comments of a removed import (other than an expanded on-demand import with + * a corresponding single import, or a reduced single import with a correponding on-demand + * import) are removed. + */ + public void testRemovedImportCommentsAreRemoved() throws Exception { + IPackageFragment pack1 = this.sourceFolder.createPackageFragment("pack1", false, null); + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("/* Socket is a very useful class */\n"); + contents.append("import java.net.Socket; // Socket to 'em!\n"); + contents.append("/* Thank goodness Java has built-in networking libraries! */\n"); + contents.append("\n"); + contents.append("import java.util.ArrayList;\n"); + contents.append("\n"); + contents.append("public class C {}\n"); + ICompilationUnit cu = pack1.createCompilationUnit("C.java", contents.toString(), false, null); + + String[] order = new String[] { "java" }; + + ImportRewrite imports = newImportsRewrite(cu, order, 99, 99, false); + imports.addImport("java.util.ArrayList"); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import java.util.ArrayList;\n"); + expected.append("\n"); + expected.append("public class C {}\n"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Addresses https://bugs.eclipse.org/318437 ("Organize Imports ignores Number of Imports needed + * for .*") and https://bugs.eclipse.org/359724 ("nested type imports not collapsed to wildcards + * ('*')"). + */ + public void testOnDemandWithinType() throws Exception { + ICompilationUnit cu = createCompilationUnit("pack1", "C"); + + String[] order = new String[] { "java" }; + + ImportRewrite imports = newImportsRewrite(cu, order, 1, 1, false); + imports.setUseContextToFilterImplicitImports(true); + imports.addImport("java.util.Map.Entry"); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import java.util.Map.*;\n"); + expected.append("\n"); + expected.append("public class C {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Expects that a comment embedded within an import declaration is preserved. + */ + public void testCommentWithinImportDeclaration() throws Exception { + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("import /* comment */ java.util.Map.*;\n"); + contents.append("\n"); + contents.append("public class C {}"); + + ICompilationUnit cu = createCompilationUnit("pack1", "C", contents.toString()); + + String[] order = new String[] { "java" }; + + ImportRewrite imports = newImportsRewrite(cu, order, 1, 1, false); + imports.setUseContextToFilterImplicitImports(true); + imports.addImport("java.util.Map.*"); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import /* comment */ java.util.Map.*;\n"); + expected.append("\n"); + expected.append("public class C {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Addresses https://bugs.eclipse.org/457051 ("comment is discarded when reducing imports to an + * on-demand import"). + */ + public void testFloatingCommentPreservedWhenReducingOnDemandAbove() throws Exception { + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("import java.util.Queue;\n"); + contents.append("\n"); + contents.append("/* floating comment */\n"); + contents.append("\n"); + contents.append("import java.util.concurrent.BlockingDeque;\n"); + contents.append("\n"); + contents.append("public class C {}"); + + ICompilationUnit cu = createCompilationUnit("pack1", "C", contents.toString()); + + String[] order = new String[] { "java" }; + + ImportRewrite importRewrite = newImportsRewrite(cu, order, 2, 2, true); + importRewrite.setUseContextToFilterImplicitImports(true); + importRewrite.addImport("java.util.Formatter"); + + apply(importRewrite); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import java.util.*;\n"); + expected.append("\n"); + expected.append("/* floating comment */\n"); + expected.append("\n"); + expected.append("import java.util.concurrent.BlockingDeque;\n"); + expected.append("\n"); + expected.append("public class C {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Addresses https://bugs.eclipse.org/457089 ("imports are improperly reordered in the presence + * of a floating comment"). + */ + public void testFloatingCommentDoesntCauseImportsToMove() throws Exception { + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("import java.io.Serializable;\n"); + contents.append("\n"); + contents.append("/* floating comment */\n"); + contents.append("\n"); + contents.append("import java.util.List;\n"); + contents.append("\n"); + contents.append("import javax.sql.DataSource;\n"); + contents.append("\n"); + contents.append("public class C {}\n"); + + ICompilationUnit cu = createCompilationUnit("pack1", "C", contents.toString()); + + String[] order = new String[] { "java", "javax" }; + + ImportRewrite importRewrite = newImportsRewrite(cu, order, 999, 999, false); + importRewrite.setUseContextToFilterImplicitImports(true); + importRewrite.addImport("java.io.Serializable"); + importRewrite.addImport("java.util.List"); + importRewrite.addImport("javax.sql.DataSource"); + + apply(importRewrite); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import java.io.Serializable;\n"); + expected.append("\n"); + expected.append("/* floating comment */\n"); + expected.append("\n"); + expected.append("import java.util.List;\n"); + expected.append("\n"); + expected.append("import javax.sql.DataSource;\n"); + expected.append("\n"); + expected.append("public class C {}\n"); + assertEqualString(cu.getSource(), expected.toString()); + } + + public void testAddImportIntoMatchAllImportGroup() throws Exception { + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("import java.util.ArrayList;\n"); + contents.append("\n"); + contents.append("public class C {}"); + + ICompilationUnit cu = createCompilationUnit("pack1", "C", contents.toString()); + + String[] order = new String[] { "", "java.net" }; + + ImportRewrite importRewrite = newImportsRewrite(cu, order, 999, 999, true); + importRewrite.setUseContextToFilterImplicitImports(true); + importRewrite.addImport("java.net.Socket"); + + apply(importRewrite); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import java.util.ArrayList;\n"); + expected.append("\n"); + expected.append("import java.net.Socket;\n"); + expected.append("\n"); + expected.append("public class C {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + public void testCuInDefaultPackageWithNoExistingImports() throws Exception { + StringBuffer contents = new StringBuffer(); + contents.append("public class C {}"); + + ICompilationUnit cu = createCompilationUnit("pack1", "C", contents.toString()); + + String[] order = new String[] { "java", "java.net" }; + + ImportRewrite importRewrite = newImportsRewrite(cu, order, 999, 999, false); + importRewrite.setUseContextToFilterImplicitImports(true); + importRewrite.addImport("java.net.Socket"); + importRewrite.addImport("java.util.ArrayList"); + + apply(importRewrite); + + StringBuffer expected = new StringBuffer(); + expected.append("import java.util.ArrayList;\n"); + expected.append("\n"); + expected.append("import java.net.Socket;\n"); + expected.append("\n"); + expected.append("public class C {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Addresses https://bugs.eclipse.org/71761 ("ImportRewrite should let me add explicit import to + * existing on demand import"). + */ + public void testNeedsExplicitImport() throws Exception { + ICompilationUnit cu = createCompilationUnit("pack1", "C"); + + String[] order = new String[] { "java" }; + + ImportRewriteContext needsExplicitImportContext = new ImportRewriteContext() { + public int findInContext(String qualifier, String name, int kind) { + return ImportRewriteContext.RES_NAME_UNKNOWN_NEEDS_EXPLICIT_IMPORT; + } + }; + + ImportRewrite importRewrite = newImportsRewrite(cu, order, 1, 1, false); + importRewrite.setUseContextToFilterImplicitImports(true); + importRewrite.addStaticImport("java.util.Collections", "shuffle", false, needsExplicitImportContext); + importRewrite.addStaticImport("java.util.Collections", "sort", false); + importRewrite.addImport("java.util.List", needsExplicitImportContext); + importRewrite.addImport("java.util.Map"); + + apply(importRewrite); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import static java.util.Collections.*;\n"); + expected.append("import static java.util.Collections.shuffle;\n"); + expected.append("\n"); + expected.append("import java.util.*;\n"); + expected.append("import java.util.List;\n"); + expected.append("\n"); + expected.append("public class C {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + public void testOrganizeNoImportsWithOneLineDelim() throws Exception { + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("public class C {}"); + + ICompilationUnit cu = createCompilationUnit("pack1", "C", contents.toString()); + + String[] order = new String[] { "java" }; + + ImportRewrite importRewrite = newImportsRewrite(cu, order, 1, 1, false); + importRewrite.setUseContextToFilterImplicitImports(true); + + apply(importRewrite); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("public class C {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + public void testOrganizeNoImportsWithTwoLineDelims() throws Exception { + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("public class C {}"); + + ICompilationUnit cu = createCompilationUnit("pack1", "C", contents.toString()); + + String[] order = new String[] { "java" }; + + ImportRewrite importRewrite = newImportsRewrite(cu, order, 1, 1, false); + importRewrite.setUseContextToFilterImplicitImports(true); + + apply(importRewrite); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("public class C {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + public void testOrganizeNoImportsWithJavadoc() throws Exception { + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("/**\n"); + contents.append(" * Best class ever.\n"); + contents.append(" */\n"); + contents.append("\n"); + contents.append("public class C {\n}"); + + ICompilationUnit cu = createCompilationUnit("pack1", "C", contents.toString()); + + String[] order = new String[] { "java" }; + + ImportRewrite importRewrite = newImportsRewrite(cu, order, 1, 1, false); + importRewrite.setUseContextToFilterImplicitImports(true); + + apply(importRewrite); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("/**\n"); + expected.append(" * Best class ever.\n"); + expected.append(" */\n"); + expected.append("\n"); + expected.append("public class C {\n}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Expects that imports are correctly placed after the end of a package declaration's multiline + * trailing comment. + */ + public void testPackageDeclarationTrailingComment() throws Exception { + IPackageFragment pack1 = this.sourceFolder.createPackageFragment("pack1", false, null); + StringBuffer contents = new StringBuffer(); + contents.append("package pack1; /* pack1 \n"); + contents.append("trailing \n"); + contents.append("comment */\n"); + contents.append("\n"); + contents.append("public class C {\n"); + contents.append("}\n"); + ICompilationUnit cu = pack1.createCompilationUnit("C.java", contents.toString(), false, null); + + String[] order = new String[] { "java" }; + + ImportRewrite imports = newImportsRewrite(cu, order, 99, 99, false); + imports.addImport("java.util.ArrayList"); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1; /* pack1 \n"); + expected.append("trailing \n"); + expected.append("comment */\n"); + expected.append("\n"); + expected.append("import java.util.ArrayList;\n"); + expected.append("\n"); + expected.append("public class C {\n"); + expected.append("}\n"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Expects correct placement of an import when package declaration, type declaration, and + * associated comments are all on one line. + */ + public void testAddImportWithPackageAndTypeOnSameLine() throws Exception { + IPackageFragment pack1 = this.sourceFolder.createPackageFragment("pack1", false, null); + StringBuffer contents = new StringBuffer(); + contents.append("package pack1; /* pack1 trailing */ /** C leading */ public class C {}\n"); + ICompilationUnit cu = pack1.createCompilationUnit("C.java", contents.toString(), false, null); + + String[] order = new String[] { "java" }; + + ImportRewrite imports = newImportsRewrite(cu, order, 99, 99, false); + imports.addImport("java.util.ArrayList"); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1; /* pack1 trailing */\n"); + expected.append("\n"); + expected.append("import java.util.ArrayList;\n"); + expected.append("\n"); + expected.append("/** C leading */ public class C {}\n"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Expects that imports not matching defined import groups are placed together at the end. + * + * Addresses https://bugs.eclipse.org/430303 ("import group sorting is broken"). + */ + public void testUnmatchedImports() throws Exception { + ICompilationUnit cu = createCompilationUnit("pack1", "C"); + + String[] order = new String[] { "java.net", "com.google" }; + + ImportRewrite imports = newImportsRewrite(cu, order, 99, 99, false); + imports.addImport("com.acme.BirdSeed"); + imports.addImport("com.acme.Dynamite"); + imports.addImport("com.google.Tgif"); + imports.addImport("java.net.Socket"); + imports.addImport("java.new.Bar"); + imports.addImport("org.linux.Kernel"); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import java.net.Socket;\n"); + expected.append("\n"); + expected.append("import com.google.Tgif;\n"); + expected.append("\n"); + expected.append("import com.acme.BirdSeed;\n"); + expected.append("import com.acme.Dynamite;\n"); + expected.append("import java.new.Bar;\n"); + expected.append("import org.linux.Kernel;\n"); + expected.append("\n"); + expected.append("public class C {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Expects that the order in which addImport is called does not affect the resulting order of + * import declarations. + * + * Addresses https://bugs.eclipse.org/430303 ("import group sorting is broken"). + */ + public void testAddImportsInVaryingOrder() throws Exception { + String[] order = new String[] { "h", "a" }; + + List importsToAdd = new ArrayList(); + importsToAdd.add("a.ClassInA"); + importsToAdd.add("b.ClassInB"); + importsToAdd.add("c.ClassInC"); + importsToAdd.add("d.ClassInD"); + importsToAdd.add("e.ClassInE"); + importsToAdd.add("f.ClassInF"); + importsToAdd.add("g.ClassInG"); + importsToAdd.add("h.ClassInH"); + + ICompilationUnit cu1 = createCompilationUnit("pack1", "C"); + ImportRewrite imports1 = newImportsRewrite(cu1, order, 99, 99, false); + for (Iterator importsToAddIter = importsToAdd.iterator(); importsToAddIter.hasNext(); ) { + imports1.addImport((String) importsToAddIter.next()); + } + apply(imports1); + String source1 = cu1.getSource(); + + Collections.reverse(importsToAdd); + + ICompilationUnit cu2 = createCompilationUnit("pack1", "C"); + ImportRewrite imports2 = newImportsRewrite(cu2, order, 99, 99, false); + for (Iterator importsToAddIter = importsToAdd.iterator(); importsToAddIter.hasNext(); ) { + imports2.addImport((String) importsToAddIter.next()); + } + apply(imports2); + String source2 = cu2.getSource(); + + // Reversing the order in which imports are added via addImport() should not affect the rewritten order. + assertEqualString(source2, source1); + } + + /** + * Expects that static imports not matching any defined import group end up above defined import + * groups and that non-static imports not matching any defined import group end up below defined + * import groups. + * + * Addresses https://bugs.eclipse.org/430303 ("import group sorting is broken"). + */ + public void testStaticAndNonStaticUnmatchedImports() throws Exception { + ICompilationUnit cu = createCompilationUnit("pack1", "C"); + + String[] order = new String[] { "#a", "h" }; + + ImportRewrite imports = newImportsRewrite(cu, order, 99, 99, false); + imports.addStaticImport("a.ClassInA", "staticMethodInA", false); + imports.addStaticImport("b.ClassInB", "staticMethodInB", false); + imports.addImport("g.ClassInG"); + imports.addImport("h.ClassInH"); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import static b.ClassInB.staticMethodInB;\n"); + expected.append("\n"); + expected.append("import static a.ClassInA.staticMethodInA;\n"); + expected.append("\n"); + expected.append("import h.ClassInH;\n"); + expected.append("\n"); + expected.append("import g.ClassInG;\n"); + expected.append("\n"); + expected.append("public class C {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Expect that two duplicate on-demand imports and their comments survive a rewrite. + */ + public void testAddWithDuplicateOnDemandImports() throws Exception { + IPackageFragment pack1 = this.sourceFolder.createPackageFragment("pack1", false, null); + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("import java.lang.*;\n"); + contents.append("\n"); + contents.append("/* foo.bar.* 1 leading */\n"); + contents.append("/* foo.bar.* 1 same-line leading */ import foo.bar.*; // foo.bar.* 1 same-line trailing\n"); + contents.append("/* foo.bar.* 1 trailing */\n"); + contents.append("\n"); + contents.append("import pack1.*;\n"); + contents.append("\n"); + contents.append("/* foo.bar.* 2 leading */\n"); + contents.append("/* foo.bar.* 2 same-line leading */ import foo.bar.*; // foo.bar.* 2 same-line trailing\n"); + contents.append("/* foo.bar.* 2 trailing */\n"); + contents.append("\n"); + contents.append("public class C {}\n"); + ICompilationUnit cu = pack1.createCompilationUnit("C.java", contents.toString(), false, null); + + String[] order = new String[] { "java.lang", "foo", "pack1", "com" }; + + ImportRewrite imports = newImportsRewrite(cu, order, 99, 99, true); + imports.addImport("com.example.MyClass"); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import java.lang.*;\n"); + expected.append("\n"); + expected.append("/* foo.bar.* 1 leading */\n"); + expected.append("/* foo.bar.* 1 same-line leading */ import foo.bar.*; // foo.bar.* 1 same-line trailing\n"); + expected.append("/* foo.bar.* 1 trailing */\n"); + expected.append("\n"); + expected.append("import pack1.*;\n"); + expected.append("\n"); + expected.append("import com.example.MyClass;\n"); + expected.append("\n"); + expected.append("/* foo.bar.* 2 leading */\n"); + expected.append("/* foo.bar.* 2 same-line leading */ import foo.bar.*; // foo.bar.* 2 same-line trailing\n"); + expected.append("/* foo.bar.* 2 trailing */\n"); + expected.append("\n"); + expected.append("public class C {}\n"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Expect that two duplicate single imports and their comments survive a rewrite. + */ + public void testAddWithDuplicateSingleImports() throws Exception { + IPackageFragment pack1 = this.sourceFolder.createPackageFragment("pack1", false, null); + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("import java.lang.*;\n"); + contents.append("\n"); + contents.append("/* foo.Bar 1 leading */\n"); + contents.append("/* foo.Bar 1 same-line leading */ import foo.Bar; // foo.Bar 1 same-line trailing\n"); + contents.append("/* foo.Bar 1 trailing */\n"); + contents.append("\n"); + contents.append("import pack1.*;\n"); + contents.append("\n"); + contents.append("/* foo.Bar 2 leading */\n"); + contents.append("/* foo.Bar 2 same-line leading */ import foo.Bar; // foo.Bar 2 same-line trailing\n"); + contents.append("/* foo.Bar 2 trailing */\n"); + contents.append("\n"); + contents.append("public class C {}"); + ICompilationUnit cu = pack1.createCompilationUnit("C.java", contents.toString(), false, null); + + String[] order = new String[] { "java.lang", "foo", "pack1", "com" }; + + ImportRewrite imports = newImportsRewrite(cu, order, 99, 99, true); + imports.addImport("com.example.MyClass"); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import java.lang.*;\n"); + expected.append("\n"); + expected.append("/* foo.Bar 1 leading */\n"); + expected.append("/* foo.Bar 1 same-line leading */ import foo.Bar; // foo.Bar 1 same-line trailing\n"); + expected.append("/* foo.Bar 1 trailing */\n"); + expected.append("\n"); + expected.append("import pack1.*;\n"); + expected.append("\n"); + expected.append("import com.example.MyClass;\n"); + expected.append("\n"); + expected.append("/* foo.Bar 2 leading */\n"); + expected.append("/* foo.Bar 2 same-line leading */ import foo.Bar; // foo.Bar 2 same-line trailing\n"); + expected.append("/* foo.Bar 2 trailing */\n"); + expected.append("\n"); + expected.append("public class C {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + public void testOtherDuplicateImportsNotDisturbed() throws Exception { + IPackageFragment pack1 = this.sourceFolder.createPackageFragment("pack1", false, null); + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("import pack1.SomeClass; // first import\n"); + contents.append("import java.util.ArrayList;\n"); + contents.append("\n"); + contents.append("import pack1.SomeClass; // second import\n"); + contents.append("import com.mycompany.Frobnigator;\n"); + contents.append("\n"); + contents.append("import pack1.SomeClass; // third import\n"); + contents.append("import org.eclipse.GreatIde;\n"); + contents.append("\n"); + contents.append("public class C {}"); + ICompilationUnit cu = pack1.createCompilationUnit("C.java", contents.toString(), false, null); + + String[] order = new String[] { "java", "pack1", "com", "org" }; + + ImportRewrite imports = newImportsRewrite(cu, order, 99, 99, true); + imports.addImport("com.mycompany.Foo"); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import pack1.SomeClass; // first import\n"); + expected.append("import java.util.ArrayList;\n"); + expected.append("\n"); + expected.append("import pack1.SomeClass; // second import\n"); + expected.append("\n"); + expected.append("import com.mycompany.Foo;\n"); + expected.append("import com.mycompany.Frobnigator;\n"); + expected.append("\n"); + expected.append("import pack1.SomeClass; // third import\n"); + expected.append("import org.eclipse.GreatIde;\n"); + expected.append("\n"); + expected.append("public class C {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + public void testDuplicateImportsDoNotCountTowardOnDemandThreshold() throws Exception { + IPackageFragment pack1 = this.sourceFolder.createPackageFragment("pack1", false, null); + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("import com.mycompany.Foo;\n"); + contents.append("import com.mycompany.Foo;\n"); + contents.append("\n"); + contents.append("public class C {}"); + ICompilationUnit cu = pack1.createCompilationUnit("C.java", contents.toString(), false, null); + + String[] order = new String[] {}; + + ImportRewrite imports = newImportsRewrite(cu, order, 3, 3, true); + imports.addImport("com.mycompany.Bar"); + + apply(imports); + + // Expect that the 3-import on-demand threshold has not been reached. + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import com.mycompany.Bar;\n"); + expected.append("import com.mycompany.Foo;\n"); + expected.append("import com.mycompany.Foo;\n"); + expected.append("\n"); + expected.append("public class C {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Expects that a conflict between identically named fields from two static on-demand imports + * is resolved with an explicit import of one of the fields. + * + * Addresses https://bugs.eclipse.org/360789 ("Organize imports changes static imports to .* + * even when that introduces compile errors"). + */ + public void testOnDemandConflictBetweenStaticFields() throws Exception { + ICompilationUnit cu = createCompilationUnit("pack1", "C"); + + // This test uses enum constants because the example in bug 360789 used enum constants, + // but the behavior generalizes to static fields that are not enum constants. + IPackageFragment pack2 = this.sourceFolder.createPackageFragment("pack2", false, null); + StringBuffer horizontalEnum = new StringBuffer(); + horizontalEnum.append("package pack2;\n"); + horizontalEnum.append("public enum Horizontal { LEFT, CENTER, RIGHT }\n"); + pack2.createCompilationUnit("Horizontal.java", horizontalEnum.toString(), false, null); + StringBuffer verticalEnum = new StringBuffer(); + verticalEnum.append("package pack2;\n"); + verticalEnum.append("public enum Vertical { TOP, CENTER, BOTTOM }\n"); + pack2.createCompilationUnit("Vertical.java", verticalEnum.toString(), false, null); + + String[] order = new String[] {}; + + ImportRewrite imports = newImportsRewrite(cu, order, 1, 1, false); + imports.addStaticImport("pack2.Horizontal", "CENTER", true); + imports.addStaticImport("pack2.Vertical", "TOP", true); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import static pack2.Horizontal.CENTER;\n"); + expected.append("import static pack2.Vertical.*;\n"); + expected.append("\n"); + expected.append("public class C {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Expects that a conflict between a static on-demand import and a type on-demand import + * is resolved with an explicit import of one of the conflicting member types. + * + * Inspired by https://bugs.eclipse.org/360789 + */ + public void testOnDemandConflictBetweenTypeAndNestedStaticType() throws Exception { + ICompilationUnit cu = createCompilationUnit("pack1", "C"); + + IPackageFragment pack2 = this.sourceFolder.createPackageFragment("pack2", false, null); + StringBuffer containingType = new StringBuffer(); + containingType.append("package pack2;\n"); + containingType.append("public class ContainingType {\n"); + containingType.append(" public static class TypeWithSameName {}\n"); + containingType.append(" public static final int CONSTANT = 42;\n"); + containingType.append("}\n"); + pack2.createCompilationUnit("ContainingType.java", containingType.toString(), false, null); + + IPackageFragment pack3 = this.sourceFolder.createPackageFragment("pack3", false, null); + StringBuffer typeWithSameName = new StringBuffer(); + typeWithSameName.append("package pack3;\n"); + typeWithSameName.append("public class TypeWithSameName {}\n"); + pack3.createCompilationUnit("TypeWithSameName.java", typeWithSameName.toString(), false, null); + + String[] order = new String[] {}; + + ImportRewrite imports = newImportsRewrite(cu, order, 1, 1, false); + imports.addStaticImport("pack2.ContainingType", "CONSTANT", true); + imports.addImport("pack3.TypeWithSameName"); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import static pack2.ContainingType.*;\n"); + expected.append("\n"); + expected.append("import pack3.TypeWithSameName;\n"); + expected.append("\n"); + expected.append("public class C {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + public void testFloatingCommentWithBlankLine() throws Exception { + IPackageFragment pack1 = this.sourceFolder.createPackageFragment("pack1", false, null); + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("import com.mycompany.Bar;\n"); + contents.append("\n"); + contents.append("/*hello!\n"); + contents.append("\n"); + contents.append("this is a comment!*/\n"); + contents.append("\n"); + contents.append("import com.mycompany.Foo;\n"); + contents.append("\n"); + contents.append("public class C {}"); + ICompilationUnit cu = pack1.createCompilationUnit("C.java", contents.toString(), false, null); + + String[] order = new String[] {}; + + ImportRewrite imports = newImportsRewrite(cu, order, 99, 99, false); + imports.addImport("com.mycompany.Bar"); + imports.addImport("com.mycompany.Foo"); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import com.mycompany.Bar;\n"); + expected.append("\n"); + expected.append("/*hello!\n"); + expected.append("\n"); + expected.append("this is a comment!*/\n"); + expected.append("\n"); + expected.append("import com.mycompany.Foo;\n"); + expected.append("\n"); + expected.append("public class C {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + /** + * Expects that an import rewrite with no effective changes produces an empty TextEdit. + */ + public void testNoEdits() throws Exception { + IPackageFragment pack1 = this.sourceFolder.createPackageFragment("pack1", false, null); + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("// leading comment\n"); + contents.append("import com.mycompany.Foo;\n"); + contents.append("// trailing comment\n"); + contents.append("\n"); + contents.append("// leading comment\n"); + contents.append("import java.util.ArrayList;\n"); + contents.append("// trailing comment\n"); + contents.append("\n"); + contents.append("public class C {}"); + ICompilationUnit cu = pack1.createCompilationUnit("C.java", contents.toString(), false, null); + + String[] order = new String[] {"com", "java"}; + + ImportRewrite imports = newImportsRewrite(cu, order, 99, 99, false); + imports.addImport("com.mycompany.Foo"); + imports.addImport("java.util.ArrayList"); + + TextEdit edit = imports.rewriteImports(null); + + assertEquals(0, ((MultiTextEdit) edit).getChildrenSize()); + } + + public void testAddImportWithCommentBetweenImportsAndType() throws Exception { + IPackageFragment pack1 = this.sourceFolder.createPackageFragment("pack1", false, null); + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("import com.mycompany.Bar;\n"); + contents.append("\n"); + contents.append("/* floating comment */\n"); + contents.append("\n"); + contents.append("// type comment\n"); + contents.append("public class C {}"); + ICompilationUnit cu = pack1.createCompilationUnit("C.java", contents.toString(), false, null); + + String[] order = new String[] {"com", "java"}; + + ImportRewrite imports = newImportsRewrite(cu, order, 99, 99, false); + imports.addImport("com.mycompany.Bar"); + imports.addImport("com.mycompany.Foo"); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import com.mycompany.Bar;\n"); + expected.append("import com.mycompany.Foo;\n"); + expected.append("\n"); + expected.append("/* floating comment */\n"); + expected.append("\n"); + expected.append("// type comment\n"); + expected.append("public class C {}"); + assertEqualString(expected.toString(), cu.getSource()); + } + + public void testRenameImportedClassWithImportedNestedClass() throws Exception { + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("import com.example.A;\n"); + contents.append("import com.example.A.ANested;\n"); + contents.append("import com.example.C;\n"); + contents.append("import com.example.C.CNested;\n"); + contents.append("import com.example.E;\n"); + contents.append("import com.example.E.ENested;\n"); + contents.append("\n"); + contents.append("public class Clazz {}"); + ICompilationUnit cu = createCompilationUnit("pack1", "Clazz", contents.toString()); + + String[] order = new String[] { "com" }; + + ImportRewrite imports = newImportsRewrite(cu, order, 999, 999, true); + imports.setUseContextToFilterImplicitImports(true); + // Simulate renaming com.example.A to com.example.D. + imports.removeImport("com.example.A"); + imports.removeImport("com.example.A.ANested"); + imports.addImport("com.example.D"); + imports.addImport("com.example.D.ANested"); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import com.example.C;\n"); + expected.append("import com.example.C.CNested;\n"); + expected.append("import com.example.D;\n"); + expected.append("import com.example.D.ANested;\n"); + expected.append("import com.example.E;\n"); + expected.append("import com.example.E.ENested;\n"); + expected.append("\n"); + expected.append("public class Clazz {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + public void testConflictsBetweenOriginalOnDemands() throws Exception { + // Create a type named "A" in each of two packages. + createCompilationUnit("conflicting1", "A"); + createCompilationUnit("conflicting2", "A"); + + // Create a static member named "doStuff" in each of two types. + StringBuffer statics1 = new StringBuffer(); + statics1.append("package statics;\n"); + statics1.append("\n"); + statics1.append("public class Statics1 {\n"); + statics1.append(" public static void doStuff() {}\n"); + statics1.append("}\n"); + createCompilationUnit("statics", "Statics1", statics1.toString()); + StringBuffer statics2 = new StringBuffer(); + statics2.append("package statics;\n"); + statics2.append("\n"); + statics2.append("public class Statics2 {\n"); + statics2.append(" public static void doStuff() {}\n"); + statics2.append("}\n"); + createCompilationUnit("statics", "Statics2", statics2.toString()); + + // Import the types and static members ambiguously via conflicting on-demand imports. + StringBuffer contents = new StringBuffer(); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("import static statics.Statics1.*;\n"); + contents.append("import static statics.Statics2.*;\n"); + contents.append("\n"); + contents.append("import conflicting1.*;\n"); + contents.append("import conflicting2.*;\n"); + contents.append("\n"); + contents.append("class Clazz {}"); + ICompilationUnit cu = createCompilationUnit("pack1", "Clazz", contents.toString()); + + ImportRewrite imports = newImportsRewrite(cu, new String[0], 1, 1, true); + imports.setUseContextToFilterImplicitImports(true); + // Add imports that surface the ambiguity between the existing on-demand imports. + imports.addImport("conflicting1.A"); + imports.addStaticImport("statics.Statics1", "doStuff", false); + + apply(imports); + + StringBuffer expected = new StringBuffer(); + // Expect that explicit single imports are added to resolve the conflicts. + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("import static statics.Statics1.*;\n"); + expected.append("import static statics.Statics1.doStuff;\n"); + expected.append("import static statics.Statics2.*;\n"); + expected.append("\n"); + expected.append("import conflicting1.*;\n"); + expected.append("import conflicting1.A;\n"); + expected.append("import conflicting2.*;\n"); + expected.append("\n"); + expected.append("class Clazz {}"); + assertEqualString(cu.getSource(), expected.toString()); + } + + public void testRemoveImportsWithPackageDocComment() throws Exception { + StringBuffer contents = new StringBuffer(); + contents.append("/** package doc comment */\n"); + contents.append("package pack1;\n"); + contents.append("\n"); + contents.append("import com.example.Foo;\n"); + contents.append("\n"); + contents.append("public class Clazz {}\n"); + ICompilationUnit cu = createCompilationUnit("pack1", "Clazz", contents.toString()); + + ImportRewrite rewrite = newImportsRewrite(cu, new String[] {}, 999, 999, true); + rewrite.setUseContextToFilterImplicitImports(true); + rewrite.removeImport("com.example.Foo"); + apply(rewrite); + + StringBuffer expected = new StringBuffer(); + expected.append("/** package doc comment */\n"); + expected.append("package pack1;\n"); + expected.append("\n"); + expected.append("public class Clazz {}\n"); + assertEqualString(cu.getSource(), expected.toString()); + } + + public void testImplicitImportFiltering() throws Exception { + String[] order = new String[] {}; + + ICompilationUnit cuWithFiltering = createCompilationUnit("pack1", "CuWithFiltering"); + + ImportRewrite rewriteWithFiltering = newImportsRewrite(cuWithFiltering, order, 999, 999, true); + rewriteWithFiltering.setUseContextToFilterImplicitImports(true); + rewriteWithFiltering.setFilterImplicitImports(true); + rewriteWithFiltering.addImport("java.lang.Integer"); + apply(rewriteWithFiltering); + + StringBuffer expectedWithFiltering = new StringBuffer(); + // Expect that the implicit java.lang import has been filtered out. + expectedWithFiltering.append("package pack1;\n"); + expectedWithFiltering.append("\n"); + expectedWithFiltering.append("public class CuWithFiltering {}"); + assertEqualString(cuWithFiltering.getSource(), expectedWithFiltering.toString()); + + ICompilationUnit cuWithoutFiltering = createCompilationUnit("pack1", "CuWithoutFiltering"); + + ImportRewrite rewriteWithoutFiltering = newImportsRewrite(cuWithoutFiltering, order, 999, 999, true); + rewriteWithoutFiltering.setUseContextToFilterImplicitImports(true); + rewriteWithoutFiltering.setFilterImplicitImports(false); + rewriteWithoutFiltering.addImport("java.lang.Integer"); + apply(rewriteWithoutFiltering); + + StringBuffer expectedWithoutFiltering = new StringBuffer(); + // Expect that the java.lang import has been added to the compilation unit. + expectedWithoutFiltering.append("package pack1;\n"); + expectedWithoutFiltering.append("\n"); + expectedWithoutFiltering.append("import java.lang.Integer;\n"); + expectedWithoutFiltering.append("\n"); + expectedWithoutFiltering.append("public class CuWithoutFiltering {}"); + assertEqualString(cuWithoutFiltering.getSource(), expectedWithoutFiltering.toString()); + } public void testAddImports1() throws Exception { @@ -101,9 +1416,9 @@ StringBuffer buf= new StringBuffer(); buf.append("package pack1;\n"); buf.append("\n"); + buf.append("import java.util.Map;\n"); buf.append("import java.util.Set;\n"); buf.append("import java.util.Vector;\n"); - buf.append("import java.util.Map;\n"); buf.append("\n"); buf.append("import pack.List;\n"); buf.append("import pack.List2;\n"); @@ -121,20 +1436,23 @@ apply(imports); + // java.net.Socket gets added to the "java" import group + // p.A gets added to the default match-all group at the end + // com.something.Foo gets added to the "com" import group buf= new StringBuffer(); buf.append("package pack1;\n"); buf.append("\n"); buf.append("import java.net.Socket;\n"); + buf.append("import java.util.Map;\n"); buf.append("import java.util.Set;\n"); buf.append("import java.util.Vector;\n"); - buf.append("import java.util.Map;\n"); buf.append("\n"); buf.append("import com.something.Foo;\n"); buf.append("\n"); - buf.append("import p.A;\n"); - buf.append("\n"); buf.append("import pack.List;\n"); buf.append("import pack.List2;\n"); + buf.append("\n"); + buf.append("import p.A;\n"); buf.append("\n"); buf.append("public class C {\n"); buf.append("}\n"); @@ -155,7 +1473,7 @@ buf.append("}\n"); ICompilationUnit cu= pack1.createCompilationUnit("C.java", buf.toString(), false, null); - String[] order= new String[] { "java.util", "java.new", "p" }; + String[] order= new String[] { "java.util", "java.net", "p" }; ImportRewrite imports= newImportsRewrite(cu, order, 2, 2, true); @@ -190,7 +1508,7 @@ buf.append("}\n"); ICompilationUnit cu= pack1.createCompilationUnit("C.java", buf.toString(), false, null); - String[] order= new String[] { "java.util", "java.new", "p" }; + String[] order= new String[] { "java.util", "java.net", "p" }; ImportRewrite imports= newImportsRewrite(cu, order, 2, 2, true); @@ -332,11 +1650,13 @@ apply(imports); + // java.util.{Map,Set,Collections} are reduced to java.util.* + // java.util.Map.Entry is reduced to java.util.Map.* buf= new StringBuffer(); buf.append("package pack1;\n"); buf.append("\n"); buf.append("import java.util.* ;\n"); - buf.append("import java.util.Map.Entry ;\n"); + buf.append("import java.util.Map.* ;\n"); buf.append("\n"); buf.append("public class C {\n"); buf.append("}\n"); @@ -371,12 +1691,13 @@ apply(imports); + // With on-demand threshold set to 1, java.util.Map.Entry is reduced to java.util.Map.*. buf= new StringBuffer(); buf.append( "package pack1;\n" + "\n" + "import java.util.*;\n" + - "import java.util.Map.Entry;\n" + + "import java.util.Map.*;\n" + "\n" + "public class C {\n" + " public static void main(String[] args) {\n" + @@ -644,7 +1965,6 @@ } public void testAddImports_bug23078() throws Exception { - IPackageFragment pack1= this.sourceFolder.createPackageFragment("pack1", false, null); StringBuffer buf= new StringBuffer(); buf.append("package pack1;\n"); @@ -657,16 +1977,58 @@ String[] order= new String[] { }; - ImportRewrite imports= newImportsRewrite(cu, order, 2, 2, true); + ImportRewrite imports= newImportsRewrite(cu, order, 3, 3, true); + imports.addImport("p.A"); imports.addImport("p.Inner"); + imports.addImport("p.Inner.*"); apply(imports); + // Without having set useContextToFilterImplicitImports to true, we get pre-3.6 behavior, + // which sorts imports by containing type and/or package before sorting by qualified name. buf= new StringBuffer(); buf.append("package pack1;\n"); buf.append("\n"); + buf.append("import p.A;\n"); buf.append("import p.Inner;\n"); buf.append("import p.A.*;\n"); + buf.append("import p.Inner.*;\n"); + buf.append("\n"); + buf.append("public class C {\n"); + buf.append("}\n"); + assertEqualString(cu.getSource(), buf.toString()); + } + + public void testAddImports_bug23078_usingContext() throws Exception { + IPackageFragment pack1= this.sourceFolder.createPackageFragment("pack1", false, null); + StringBuffer buf= new StringBuffer(); + buf.append("package pack1;\n"); + buf.append("\n"); + buf.append("import p.A.*;\n"); + buf.append("\n"); + buf.append("public class C {\n"); + buf.append("}\n"); + ICompilationUnit cu= pack1.createCompilationUnit("C.java", buf.toString(), false, null); + + String[] order= new String[] { }; + + ImportRewrite imports= newImportsRewrite(cu, order, 3, 3, true); + imports.setUseContextToFilterImplicitImports(true); + imports.addImport("p.A"); + imports.addImport("p.Inner"); + imports.addImport("p.Inner.*"); + + apply(imports); + + // Having set useContextToFilterImplicitImports to true, we get 3.6-and-later behavior, + // which sorts imports by containing package and then by qualified name. + buf= new StringBuffer(); + buf.append("package pack1;\n"); + buf.append("\n"); + buf.append("import p.A;\n"); + buf.append("import p.A.*;\n"); + buf.append("import p.Inner;\n"); + buf.append("import p.Inner.*;\n"); buf.append("\n"); buf.append("public class C {\n"); buf.append("}\n"); @@ -737,12 +2099,14 @@ assertEqualString(cu.getSource(), buf.toString()); } + /** + * Expects that, in the absence of a package declaration, comments preceding the first import + * declaration are treated as file header comments and left in place. + */ public void testAddImports_bug121428() throws Exception { IPackageFragment pack1= this.sourceFolder.createPackageFragment("pack1", false, null); StringBuffer buf= new StringBuffer(); - buf.append("package pack1;\n"); - buf.append("\n"); buf.append("/** comment */\n"); buf.append("import java.lang.System;\n"); buf.append("\n"); @@ -758,8 +2122,6 @@ apply(imports); buf= new StringBuffer(); - buf.append("package pack1;\n"); - buf.append("\n"); buf.append("/** comment */\n"); buf.append("import java.io.Exception;\n"); buf.append("\n"); @@ -1240,7 +2602,8 @@ public void testPackageInfo() throws Exception { IPackageFragment pack1= this.sourceFolder.createPackageFragment("pack1", false, null); StringBuffer buf= new StringBuffer(); - buf.append("\npackage pack1;"); + buf.append("\n"); + buf.append("package pack1;"); ICompilationUnit cu= pack1.createCompilationUnit("package-info.java", buf.toString(), false, null); @@ -1252,7 +2615,9 @@ apply(imports); buf= new StringBuffer(); - buf.append("\npackage pack1;\n"); + buf.append("\n"); + buf.append("package pack1;\n"); + buf.append("\n"); buf.append("import foo.Bar;\n"); assertEqualString(cu.getSource(), buf.toString()); } @@ -1315,15 +2680,25 @@ assertEqualString(units[2].getSource(), buf.toString()); } + /** + * Expects that comments in a variety of positions around and between import declarations + * are preserved when restoreExistingImports is set to false. + */ public void testAddImports_bug24804() throws Exception { - IPackageFragment pack1= this.sourceFolder.createPackageFragment("pack1", false, null); StringBuffer buf= new StringBuffer(); buf.append("package pack1;\n"); buf.append("\n"); - buf.append("import java.lang.String;\n"); - buf.append("/** comment */\n"); - buf.append("import java.lang.System;\n"); + buf.append("/** floating comment before first import */\n"); + buf.append("\n"); + buf.append("import java.util.ArrayList; // trailing same-line comment\n"); + buf.append("\n"); + buf.append("/** floating comment between imports*/\n"); + buf.append("\n"); + buf.append("/** preceding-line comment */\n"); + buf.append("import java.util.Collection;\n"); + buf.append("/** comment on line between imports */\n"); + buf.append("import java.util.Deque;\n"); buf.append("\n"); buf.append("public class C {\n"); buf.append("}\n"); @@ -1332,169 +2707,31 @@ String[] order= new String[] { "java" }; ImportRewrite imports= newImportsRewrite(cu, order, 99, 99, false); - imports.addImport("java.io.Exception"); + imports.addImport("java.util.ArrayList"); + imports.addImport("java.util.Collection"); + imports.addImport("java.util.Deque"); apply(imports); buf= new StringBuffer(); buf.append("package pack1;\n"); buf.append("\n"); - buf.append("import java.io.Exception;\n"); - buf.append("/** comment */\n"); + buf.append("/** floating comment before first import */\n"); + buf.append("\n"); + buf.append("import java.util.ArrayList; // trailing same-line comment\n"); + buf.append("\n"); + buf.append("/** floating comment between imports*/\n"); + buf.append("\n"); + buf.append("/** preceding-line comment */\n"); + buf.append("import java.util.Collection;\n"); + buf.append("/** comment on line between imports */\n"); + buf.append("import java.util.Deque;\n"); buf.append("\n"); buf.append("public class C {\n"); buf.append("}\n"); assertEqualString(cu.getSource(), buf.toString()); } - public void testAddImports_bug24804_2() throws Exception { - - IPackageFragment pack1= this.sourceFolder.createPackageFragment("pack1", false, null); - StringBuffer buf= new StringBuffer(); - buf.append("package pack1;\n"); - buf.append("\n"); - buf.append("import java.lang.AssertionError;//test\n"); - buf.append("\n"); - buf.append("/** comment2 */\n"); - buf.append("\n"); - buf.append("/** comment */\n"); - buf.append("import java.lang.System;\n"); - buf.append("\n"); - buf.append("public class C {\n"); - buf.append("}\n"); - ICompilationUnit cu= pack1.createCompilationUnit("C.java", buf.toString(), false, null); - - String[] order= new String[] { "java" }; - - ImportRewrite imports= newImportsRewrite(cu, order, 99, 99, true); - imports.addImport("java.io.Exception"); - - apply(imports); - - buf= new StringBuffer(); - buf.append("package pack1;\n"); - buf.append("\n"); - buf.append("import java.io.Exception;\n"); - buf.append("import java.lang.AssertionError;//test\n"); - buf.append("\n"); - buf.append("/** comment2 */\n"); - buf.append("\n"); - buf.append("/** comment */\n"); - buf.append("import java.lang.System;\n"); - buf.append("\n"); - buf.append("public class C {\n"); - buf.append("}\n"); - assertEqualString(cu.getSource(), buf.toString()); - } - - public void testAddImports_bug24804_3() throws Exception { - - IPackageFragment pack1= this.sourceFolder.createPackageFragment("pack1", false, null); - StringBuffer buf= new StringBuffer(); - buf.append("package pack1;\n"); - buf.append("\n"); - buf.append("import java.lang.String;//test\n"); - buf.append("/** comment */\n"); - buf.append("import java.lang.System;\n"); - buf.append("\n"); - buf.append("public class C {\n"); - buf.append("}\n"); - ICompilationUnit cu= pack1.createCompilationUnit("C.java", buf.toString(), false, null); - - String[] order= new String[] { "java" }; - - ImportRewrite imports= newImportsRewrite(cu, order, 99, 99, false); - imports.addImport("java.io.Exception"); - - apply(imports); - - buf= new StringBuffer(); - buf.append("package pack1;\n"); - buf.append("\n"); - buf.append("import java.io.Exception;\n"); - buf.append("//test\n"); - buf.append("/** comment */\n"); - buf.append("\n"); - buf.append("public class C {\n"); - buf.append("}\n"); - assertEqualString(cu.getSource(), buf.toString()); - } - - public void testAddImports_bug24804_4() throws Exception { - - IPackageFragment pack1= this.sourceFolder.createPackageFragment("pack1", false, null); - StringBuffer buf= new StringBuffer(); - buf.append("package pack1;\n"); - buf.append("\n"); - buf.append("import java.lang.AssertionError;//test\n"); - buf.append("\n"); - buf.append("/** comment2 */\n"); - buf.append("\n"); - buf.append("/** comment */\n"); - buf.append("import java.lang.System; /** comment3 */\n"); - buf.append("\n"); - buf.append("public class C {\n"); - buf.append("}\n"); - ICompilationUnit cu= pack1.createCompilationUnit("C.java", buf.toString(), false, null); - - String[] order= new String[] { "java" }; - - ImportRewrite imports= newImportsRewrite(cu, order, 99, 99, false); - imports.addImport("java.io.Exception"); - - apply(imports); - - buf= new StringBuffer(); - buf.append("package pack1;\n"); - buf.append("\n"); - buf.append("import java.io.Exception;\n"); - buf.append("//test\n"); - buf.append("/** comment2 */\n"); - buf.append("/** comment */\n"); - buf.append("/** comment3 */\n"); - buf.append("\n"); - buf.append("public class C {\n"); - buf.append("}\n"); - assertEqualString(cu.getSource(), buf.toString()); - } - - public void testAddImports_bug24804_5() throws Exception { - - IPackageFragment pack1= this.sourceFolder.createPackageFragment("pack1", false, null); - StringBuffer buf= new StringBuffer(); - buf.append("package pack1;\n"); - buf.append("\n"); - buf.append("import java.lang.AssertionError; //test\n"); - buf.append("\n"); - buf.append("/** comment2 */\n"); - buf.append("\n"); - buf.append("/** comment */\n"); - buf.append("import java.lang.System;\n"); - buf.append("\n"); - buf.append("public class C {\n"); - buf.append("}\n"); - ICompilationUnit cu= pack1.createCompilationUnit("C.java", buf.toString(), false, null); - - String[] order= new String[] { "java" }; - - ImportRewrite imports= newImportsRewrite(cu, order, 1, 1, false); - imports.addImport("java.io.Exception"); - - apply(imports); - - buf= new StringBuffer(); - buf.append("package pack1;\n"); - buf.append("\n"); - buf.append("import java.io.*;\n"); - buf.append("//test\n"); - buf.append("/** comment2 */\n"); - buf.append("/** comment */\n"); - buf.append("\n"); - buf.append("public class C {\n"); - buf.append("}\n"); - assertEqualString(cu.getSource(), buf.toString()); - } - // https://bugs.eclipse.org/bugs/show_bug.cgi?id=376930 public void testBug376930() throws Exception { IPackageFragment pack1 = this.sourceFolder.createPackageFragment("pack1", false, null); @@ -1595,9 +2832,9 @@ buf.append( "package pack1;\n" + "\n" + - "// comment 1\n" + "import java.io.*;\n" + "\n" + + "// comment 1\n" + "import java.util.*; // test\n" + "import java.util.Map.Entry; // test2\n" + "import java.util.Map.SomethingElse;\n" + @@ -1657,10 +2894,10 @@ buf.append( "package pack1;\n" + "\n" + - "// comment 1\n" + - "/* lead 1*/ import java.io.*;\n" + + "import java.io.*;\n" + "\n" + - "import java.util.*; // test1\n" + + "// comment 1\n" + + "/* lead 1*/ import java.util.*; // test1\n" + "/* lead 2*/import java.util.Map.Entry; // test2\n" + "/* lead 3*/ import java.util.Map.SomethingElse; // test3\n" + "// commen 3\n" + @@ -1718,16 +2955,8 @@ buf.append( "package pack1;\n" + "\n" + - "// comment 1\n" + - "/* lead 1*/ " + "import java.io.*;\n" + - "// test1\n" + - "/* lead 2*/\n" + - "// test2\n" + - "/* lead 3*/ \n" + - "// test3\n" + - "// commen 3\n" + - "\n" + + "\n" + "public class C {\n" + " public static void main(String[] args) {\n" + " HashMap h;\n" + @@ -1783,10 +3012,10 @@ buf.append( "package pack1;\n" + "\n" + - "// comment 1\n" + - "/* lead 1*/ import java.io.*;\n" + + "import java.io.*;\n" + "\n" + - "import java.util.*; // test1\n" + + "// comment 1\n" + + "/* lead 1*/ import java.util.*; // test1\n" + "/* lead 2*/import java.util.Map.*; // test2\n" + "/* lead 3*/ import java.util.Map.SomethingElse; // test3\n" + "// commen 3\n" + @@ -1844,16 +3073,8 @@ buf.append( "package pack1;\n" + "\n" + - "// comment 1\n" + - "/* lead 1*/ " + "import java.io.*;\n" + - "// test1\n" + - "/* lead 2*/\n" + - "// test2\n" + - "/* lead 3*/ \n" + - "// test3\n" + - "// commen 3\n" + - "\n" + + "\n" + "public class C {\n" + " public static void main(String[] args) {\n" + " HashMap h;\n" + @@ -1906,18 +3127,20 @@ apply(imports); buf = new StringBuffer(); + // java.util.Map.* is placed after java.util.* and is assigned the comments + // from java.util.Map.SomethingElse. buf.append( "package pack1;\n" + "\n" + "// comment 1\n" + "/* lead 1*/ import java.util.*; // test1\n" + + "/* lead 3*/\n" + + "// test3\n" + + "// commen 3\n" + "import java.util.Map.*;\n" + "\n" + "/* lead 2*/import java.io.PrintWriter.*; // test2\n" + "\n" + - "/* lead 3*/ import java.util.Map.SomethingElse; // test3\n" + - "// commen 3\n" + - "\n" + "public class C {\n" + " public static void main(String[] args) {\n" + " HashMap h;\n" + @@ -1969,17 +3192,19 @@ apply(imports); + // java.util.Map.* takes the place of java.util.Map.SomethingElse, + // and the latter's comments are reassigned to it. buf = new StringBuffer(); buf.append( "package pack1;\n" + "\n" + "// comment 1\n" + "/* lead 2*/import java.io.PrintWriter.*; // test2\n" + - "\n" + "/* lead 1*/ import java.util.*; // test1\n" + - "import java.util.Map.*;\n" + - "/* lead 3*/ import java.util.Map.SomethingElse; // test3\n" + + "/* lead 3*/\n" + + "// test3\n" + "// commen 3\n" + + "import java.util.Map.*;\n" + "\n" + "public class C {\n" + " public static void main(String[] args) {\n" + @@ -2034,13 +3259,8 @@ buf.append( "package pack1;\n" + "\n" + - "// comment 1\n" + - "/* lead 2*//* lead 1*/ import java.util.*; // test1\n" + - "// test2\n" + - "/* lead 3*/ \n" + - "// test3\n" + - "// commen 3\n" + - "\n" + + "/* lead 1*/ import java.util.*; // test1\n" + + "\n" + "public class C {\n" + " public static void main(String[] args) {\n" + " HashMap h;\n" + @@ -2090,19 +3310,17 @@ apply(imports); + // java.util.Map.* takes the place of java.util.Map.SomethingElse, + // and the latter's comments are reassigned to it. buf = new StringBuffer(); buf.append( "package pack1;\n" + "\n" + - "// comment 1\n" + - "/* lead 1*/ " + - "import java.util.Map.*; // test1\n" + - "/* lead 2*/\n" + - "// test2\n" + - "/* lead 3*/ \n" + + "/* lead 3*/\n" + "// test3\n" + "// commen 3\n" + - "\n" + + "import java.util.Map.*;\n" + + "\n" + "public class C {\n" + " public static void main(String[] args) {\n" + " HashMap h;\n" + @@ -2158,9 +3376,10 @@ "// comment 1\n" + "/* lead 2*/import java.io.PrintWriter.*; // test2\n" + "\n" + - "/* lead 1*/ import java.util.*;\n" + - " // test1\n" + + "/* lead 1*/\n" + + "// test1\n" + "// commen 3\n" + + "import java.util.*;\n" + "\n" + "public class C {\n" + " public static void main(String[] args) {\n" + @@ -2347,17 +3566,20 @@ " * don't move me 1\n" + " *\n" + " */\n" + - "import java.awt.*;// test1\n" + + "// test1\n" + + "import java.awt.*;\n" + "/*\n" + " * don't move me 2\n" + " */\n" + - "import java.io.*;// test2\n" + + "// test2\n" + + "import java.io.*;\n" + "\n" + "/*\n" + " * don't move me 3\n" + " */\n" + - "import java.util.*;// test3\n" + + "// test3\n" + "// commen 3\n" + + "import java.util.*;\n" + "\n" + "public class C implements Serializable{\n" + " public static void main(String[] args) {\n" + @@ -2435,20 +3657,24 @@ "// lead 1\n" + "import java.awt.List;// test1\n" + "\n" + - "//lead 3\n" + - "import java.util.HashMap;// test3\n" + - "// commen 3\n" + "/*\n" + " * don't move me 2\n" + " */\n" + + "\n" + "// lead 2\n" + "import java.io.Serializable;// test2\n" + "/*\n" + " * don't move me 3\n" + " */\n" + + "\n" + "/*\n" + " * don't move me 4\n" + " */\n" + + "\n" + + "//lead 3\n" + + "import java.util.HashMap;// test3\n" + + "// commen 3\n" + + "\n" + "public class C implements Serializable{\n" + " public static void main(String[] args) {\n" + " List l = new List();\n" + @@ -2524,17 +3750,21 @@ "\n" + "// lead 1\n" + "import java.awt.List;// test1\n" + + "\n" + "/*\n" + " * don't move me 2\n" + " */\n" + + "\n" + "// lead 2\n" + "import java.io.Serializable;// test2\n" + "/*\n" + " * don't move me 3\n" + " */\n" + + "\n" + "/*\n" + " * don't move me 4\n" + " */\n" + + "\n" + "//lead 3\n" + "import java.util.HashMap;// test3\n" + "// commen 3\n" + @@ -2613,21 +3843,28 @@ " */\n" + "\n" + "// lead 1\n" + - "import java.awt.*;// test1\n" + + "// test1\n" + + "import java.awt.*;\n" + + "\n" + "/*\n" + " * don't move me 2\n" + " */\n" + + "\n" + "// lead 2\n" + - "import java.io.*;// test2\n" + + "// test2\n" + "/*\n" + " * don't move me 3\n" + " */\n" + + "import java.io.*;\n" + + "\n" + "/*\n" + " * don't move me 4\n" + " */\n" + + "\n" + "//lead 3\n" + - "import java.util.*;// test3\n" + + "// test3\n" + "// commen 3\n" + + "import java.util.*;\n" + "\n" + "public class C implements Serializable{\n" + " public static void main(String[] args) {\n" + @@ -2701,22 +3938,17 @@ " */\n" + "\n" + "// lead 1\n" + - "import java.awt.*;// test1\n" + + "// test1\n" + + "import java.awt.*;\n" + "\n" + - "//lead 3\n" + - "import java.util.*;// test3\n" + - "// commen 3\n" + "/*\n" + " * don't move me 4\n" + - " */" + - "/*\n" + - " * don't move me 2\n" + " */\n" + - "// lead 2\n" + - "// test2\n" + - "/*\n" + - " * don't move me 3\n" + - " */\n" + + "\n" + + "//lead 3\n" + + "// test3\n" + + "// commen 3\n" + + "import java.util.*;\n" + "\n" + "public class C implements Serializable{\n" + " public static void main(String[] args) {\n" + @@ -2793,18 +4025,22 @@ "\n" + "// lead 1\n" + "import java.awt.List;// test1\n" + + "\n" + "/*\n" + " * don't move me 2\n" + " */\n" + + "\n" + "// lead 2\n" + - "import java.io.*;\n" + "// test2\n" + "/*\n" + " * don't move me 3\n" + " */\n" + + "import java.io.*;\n" + + "\n" + "/*\n" + " * don't move me 4\n" + " */\n" + + "\n" + "//lead 3\n" + "import java.util.HashMap;// test3\n" + "// commen 3\n" + @@ -2886,21 +4122,14 @@ "import java.awt.List;// test1\n" + "import java.io.PrintWriter;\n" + "\n" + + "/*\n" + + " * don't move me 4\n" + + " */\n" + + "\n" + "//lead 3\n" + "import java.util.HashMap;// test3\n" + "// commen 3\n" + - "/*\n" + - " * don't move me 4\n" + - " */" + - "/*\n" + - " * don't move me 2\n" + - " */\n" + - "// lead 2\n" + - "// test2\n" + - "/*\n" + - " * don't move me 3\n" + - " */\n" + - "\n" + + "\n" + "public class C implements Serializable{\n" + " public static void main(String[] args) {\n" + " List l = new List();\n" + @@ -2976,18 +4205,22 @@ "\n" + "// lead 1\n" + "import java.awt.List;// test1\n" + + "\n" + "/*\n" + " * don't move me 2\n" + " */\n" + + "\n" + "// lead 2\n" + - "import java.io.*;\n" + "// test2\n" + "/*\n" + " * don't move me 3\n" + " */\n" + + "import java.io.*;\n" + + "\n" + "/*\n" + " * don't move me 4\n" + " */\n" + + "\n" + "//lead 3\n" + "import java.util.HashMap;// test3\n" + "// commen 3\n" + @@ -3074,25 +4307,30 @@ "// lead 1\n" + "import java.awt.List;// test1\n" + "\n" + - "//lead 4\n" + - "import java.util.HashMap;// test4\n" + - "// commen 3\n" + "/*\n" + " * keep me with Serializable\n" + " */\n" + + "\n" + "// lead 2\n" + - "// lead 3\n" + - "import java.io.*;// test3\n" + - "/*\n" + - " * keep me with PrintWriter\n" + - " */\n" + "// test2\n" + "/*\n" + " * keep me with Serializable 2\n" + " */\n" + + "// lead 3\n" + + "// test3\n" + + "/*\n" + + " * keep me with PrintWriter\n" + + " */\n" + + "import java.io.*;\n" + + "\n" + "/*\n" + " * don't move me\n" + " */\n" + + "\n" + + "//lead 4\n" + + "import java.util.HashMap;// test4\n" + + "// commen 3\n" + + "\n" + "public class C implements Serializable{\n" + " public static void main(String[] args) {\n" + " List l = new List();\n" + @@ -3165,23 +4403,28 @@ " */\n" + "\n" + "// lead 1\n" + - "import java.awt.*;// test1\n" + + "// test1\n" + + "import java.awt.*;\n" + "\n" + "//lead 3\n" + - "import java.util.*;// test3\n" + + "// test3\n" + "// commen 3\n" + + "import java.util.*;\n" + + "\n" + + "/*\n" + + " * don't move me 2\n" + + " */\n" + "\n" + "// lead 2\n" + - "import java.io.*;// test2\n" + + "// test2\n" + "/*\n" + " * don't move me 3\n" + " */\n" + "/*\n" + " * don't move me 4\n" + " */\n" + - "/*\n" + - " * don't move me 2\n" + - " */\n" + + "import java.io.*;\n" + + "\n" + "public class C implements Serializable{\n" + " public static void main(String[] args) {\n" + " List l = new List();\n" + @@ -3246,14 +4489,6 @@ buf.append( "package pack1;\n" + "\n" + - "// comment 1\n" + - "/*\n" + - " * don't move me 1\n" + - " *\n" + - " */\n" + - "\n" + - "// lead 1\n" + - "\n" + "/*\n" + " * don't move me 2\n" + " */\n" + @@ -3267,7 +4502,6 @@ "/*\n" + " * don't move me 4\n" + " */\n" + - "\n" + "\n" + "//lead 3\n" + "import java.util.HashMap;// test3\n" + @@ -3336,14 +4570,6 @@ buf.append( "package pack1;\n" + "\n" + - "// comment 1\n" + - "/*\n" + - " * don't move me 1\n" + - " *\n" + - " */\n" + - "// lead 1\n" + - "/* i am with List */\n" + - "\n" + "/*\n" + " * don't move me 2\n" + " */\n" + @@ -3357,7 +4583,6 @@ "/*\n" + " * don't move me 4\n" + " */\n" + - "\n" + "\n" + "//lead 3\n" + "import java.util.HashMap;// test3\n" + @@ -3437,23 +4662,30 @@ " *\n" + " */\n" + "// lead 1\n" + - "import java.awt.List;// test1\n" + + "// test1\n" + "/* i am with List */\n" + + "import java.awt.List;\n" + + "\n" + "/*\n" + " * don't move me 2\n" + " */\n" + + "\n" + "// lead 2\n" + - "import java.io.PrintWriter;// test2\n" + + "// test2\n" + "/*\n" + " * don't move me 3\n" + " */\n" + + "import java.io.PrintWriter;\n" + "import java.io.Serializable;\n" + + "\n" + "/*\n" + " * don't move me 4\n" + " */\n" + + "\n" + "//lead 3\n" + - "import java.util.HashMap;// test3\n" + + "// test3\n" + "// commen 3\n" + + "import java.util.HashMap;\n" + "import java.util.Map;\n" + "\n" + "public class C implements Serializable{\n" + @@ -3533,26 +4765,33 @@ " *\n" + " */\n" + "// lead 1\n" + - "import java.awt.*;// test1\n" + + "// test1\n" + "/* i am with List */\n" + + "import java.awt.*;\n" + "\n" + - "//lead 3\n" + - "import java.util.*;// test3\n" + - "/*\n" + - " * don't move me 3\n" + - " */\n" + - "/*keep me with Map.Entry*/\n" + - "import java.util.Map.Entry;// member type import\n" + - "/*keep me with Map.Entry 2*/\n" + - "/*\n" + - " * don't move me 2\n" + - " */" + "/*\n" + " * don't move me 4\n" + " */\n" + + "\n" + "// lead 2\n" + - "import java.io.*;// test2\n" + + "// test2\n" + "// commen 3\n" + + "import java.io.*;\n" + + "\n" + + "/*\n" + + " * don't move me 2\n" + + " */\n" + + "\n" + + "//lead 3\n" + + "// test3\n" + + "/*\n" + + " * don't move me 3\n" + + " */\n" + + "import java.util.*;\n" + + "/*keep me with Map.Entry*/\n" + + "// member type import\n" + + "/*keep me with Map.Entry 2*/\n" + + "import java.util.Map.*;\n" + "\n" + "public class C implements Serializable{\n" + " public static void main(String[] args) {\n" + @@ -3613,6 +4852,21 @@ assertEquals("pack1.X", actualType.toString()); } + private ICompilationUnit createCompilationUnit(String packageName, String className) throws JavaModelException { + StringBuffer contents = new StringBuffer(); + contents.append("package " + packageName + ";\n"); + contents.append("\n"); + contents.append("public class " + className + " {}"); + return createCompilationUnit(packageName, className, contents.toString()); + } + + private ICompilationUnit createCompilationUnit( + String packageName, String className, String contents) throws JavaModelException { + IPackageFragment pack1 = this.sourceFolder.createPackageFragment(packageName, false, null); + ICompilationUnit cu = pack1.createCompilationUnit(className + ".java", contents, /* force */ true, null); + return cu; + } + private void assertAddedAndRemoved(ImportRewrite imports, String[] expectedAdded, String[] expectedRemoved, String[] expectedAddedStatic, String[] expectedRemovedStatic) { assertEqualStringsIgnoreOrder(imports.getAddedImports(), expectedAdded); assertEqualStringsIgnoreOrder(imports.getAddedStaticImports(), expectedAddedStatic); diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/rewrite/ImportRewrite.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/rewrite/ImportRewrite.java index 9043aac..110a27e 100644 --- a/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/rewrite/ImportRewrite.java +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/core/dom/rewrite/ImportRewrite.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2014 IBM Corporation and others. + * Copyright (c) 2000, 2015 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -7,14 +7,18 @@ * * Contributors: * IBM Corporation - initial API and implementation + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 *******************************************************************************/ package org.eclipse.jdt.core.dom.rewrite; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; @@ -62,7 +66,10 @@ import org.eclipse.jdt.core.dom.Type; import org.eclipse.jdt.core.dom.TypeLiteral; import org.eclipse.jdt.core.dom.WildcardType; -import org.eclipse.jdt.internal.core.dom.rewrite.ImportRewriteAnalyzer; +import org.eclipse.jdt.internal.core.dom.rewrite.imports.ImportRewriteConfiguration; +import org.eclipse.jdt.internal.core.dom.rewrite.imports.ImportRewriteAnalyzer; +import org.eclipse.jdt.internal.core.dom.rewrite.imports.ImportRewriteConfiguration.ImplicitImportIdentification; +import org.eclipse.jdt.internal.core.dom.rewrite.imports.ImportRewriteConfiguration.ImportContainerSorting; import org.eclipse.jdt.internal.core.util.Messages; import org.eclipse.jdt.internal.core.util.Util; import org.eclipse.text.edits.MultiTextEdit; @@ -122,6 +129,14 @@ public final static int RES_NAME_CONFLICT= 3; /** + * Result constant signaling that the given element must be imported explicitly (and must not be folded into + * an on-demand import or filtered as an implicit import). + * + * @since 3.11 + */ + public final static int RES_NAME_UNKNOWN_NEEDS_EXPLICIT_IMPORT= 4; + + /** * Kind constant specifying that the element is a type import. */ public final static int KIND_TYPE= 1; @@ -138,14 +153,17 @@ /** * Searches for the given element in the context and reports if the element is known ({@link #RES_NAME_FOUND}), - * unknown ({@link #RES_NAME_UNKNOWN}) or if its name conflicts ({@link #RES_NAME_CONFLICT}) with an other element. + * unknown ({@link #RES_NAME_UNKNOWN}), unknown in the context but known to require an explicit import + * ({@link #RES_NAME_UNKNOWN_NEEDS_EXPLICIT_IMPORT}), or if its name conflicts ({@link #RES_NAME_CONFLICT}) + * with an other element. + * * @param qualifier The qualifier of the element, can be package or the qualified name of a type * @param name The simple name of the element; either a type, method or field name or * for on-demand imports. * @param kind The kind of the element. Can be either {@link #KIND_TYPE}, {@link #KIND_STATIC_FIELD} or * {@link #KIND_STATIC_METHOD}. Implementors should be prepared for new, currently unspecified kinds and return * {@link #RES_NAME_UNKNOWN} by default. - * @return Returns the result of the lookup. Can be either {@link #RES_NAME_FOUND}, {@link #RES_NAME_UNKNOWN} or - * {@link #RES_NAME_CONFLICT}. + * @return Returns the result of the lookup. Can be either {@link #RES_NAME_FOUND}, {@link #RES_NAME_UNKNOWN}, + * {@link #RES_NAME_CONFLICT}, or {@link #RES_NAME_UNKNOWN_NEEDS_EXPLICIT_IMPORT}. */ public abstract int findInContext(String qualifier, String name, int kind); } @@ -166,8 +184,20 @@ private int importOnDemandThreshold; private int staticImportOnDemandThreshold; - private List addedImports; - private List removedImports; + private List addedImports; + private List removedImports; + + /** + * Simple names of non-static imports which must not be reduced into on-demand imports + * or filtered out as implicit. + */ + private Set typeExplicitSimpleNames; + + /** + * Simple names of static imports which must not be reduced into on-demand imports + * or filtered out as implicit. + */ + private Set staticExplicitSimpleNames; private String[] createdImports; private String[] createdStaticImports; @@ -266,8 +296,10 @@ return findInImports(qualifier, name, kind); } }; - this.addedImports= null; // Initialized on use - this.removedImports= null; // Initialized on use + this.addedImports= new ArrayList(); + this.removedImports= new ArrayList(); + this.typeExplicitSimpleNames = new HashSet(); + this.staticExplicitSimpleNames = new HashSet(); this.createdImports= null; this.createdStaticImports= null; @@ -984,6 +1016,10 @@ if (res == ImportRewriteContext.RES_NAME_UNKNOWN) { addEntry(STATIC_PREFIX + key); } + if (res == ImportRewriteContext.RES_NAME_UNKNOWN_NEEDS_EXPLICIT_IMPORT) { + addEntry(STATIC_PREFIX + key); + this.staticExplicitSimpleNames.add(simpleName); + } return simpleName; } @@ -1012,35 +1048,31 @@ if (res == ImportRewriteContext.RES_NAME_UNKNOWN) { addEntry(NORMAL_PREFIX + fullTypeName); } + if (res == ImportRewriteContext.RES_NAME_UNKNOWN_NEEDS_EXPLICIT_IMPORT) { + addEntry(NORMAL_PREFIX + fullTypeName); + this.typeExplicitSimpleNames.add(typeName); + } return typeName; } private void addEntry(String entry) { this.existingImports.add(entry); - if (this.removedImports != null) { - if (this.removedImports.remove(entry)) { - return; - } + if (this.removedImports.remove(entry)) { + return; } - if (this.addedImports == null) { - this.addedImports= new ArrayList(); - } this.addedImports.add(entry); } private boolean removeEntry(String entry) { if (this.existingImports.remove(entry)) { - if (this.addedImports != null) { - if (this.addedImports.remove(entry)) { - return true; - } + if (this.addedImports.remove(entry)) { + return true; } - if (this.removedImports == null) { - this.removedImports= new ArrayList(); - } + this.removedImports.add(entry); + return true; } return false; @@ -1118,38 +1150,64 @@ usedAstRoot= (CompilationUnit) parser.createAST(new SubProgressMonitor(monitor, 1)); } + ImportRewriteConfiguration config= buildImportRewriteConfiguration(); + ImportRewriteAnalyzer computer= - new ImportRewriteAnalyzer( - this.compilationUnit, - usedAstRoot, - this.importOrder, - this.importOnDemandThreshold, - this.staticImportOnDemandThreshold, - this.restoreExistingImports, - this.useContextToFilterImplicitImports); - computer.setFilterImplicitImports(this.filterImplicitImports); + new ImportRewriteAnalyzer(this.compilationUnit, usedAstRoot, config); - if (this.addedImports != null) { - for (int i= 0; i < this.addedImports.size(); i++) { - String curr= (String) this.addedImports.get(i); - computer.addImport(curr.substring(1), STATIC_PREFIX == curr.charAt(0), usedAstRoot, this.restoreExistingImports); - } + for (String addedImport : this.addedImports) { + boolean isStatic = STATIC_PREFIX == addedImport.charAt(0); + String qualifiedName = addedImport.substring(1); + computer.addImport(isStatic, qualifiedName); } - if (this.removedImports != null) { - for (int i= 0; i < this.removedImports.size(); i++) { - String curr= (String) this.removedImports.get(i); - computer.removeImport(curr.substring(1), STATIC_PREFIX == curr.charAt(0)); - } + for (String removedImport : this.removedImports) { + boolean isStatic = STATIC_PREFIX == removedImport.charAt(0); + String qualifiedName = removedImport.substring(1); + computer.removeImport(isStatic, qualifiedName); } - TextEdit result= computer.getResultingEdits(new SubProgressMonitor(monitor, 1)); - this.createdImports= computer.getCreatedImports(); - this.createdStaticImports= computer.getCreatedStaticImports(); - return result; + for (String typeExplicitSimpleName : this.typeExplicitSimpleNames) { + computer.requireExplicitImport(false, typeExplicitSimpleName); + } + + for (String staticExplicitSimpleName : this.staticExplicitSimpleNames) { + computer.requireExplicitImport(true, staticExplicitSimpleName); + } + + ImportRewriteAnalyzer.RewriteResult result= computer.analyzeRewrite(new SubProgressMonitor(monitor, 1)); + + this.createdImports= result.getCreatedImports(); + this.createdStaticImports= result.getCreatedStaticImports(); + + return result.getTextEdit(); } finally { monitor.done(); } + } + + private ImportRewriteConfiguration buildImportRewriteConfiguration() { + ImportRewriteConfiguration.Builder configBuilder; + + if (this.restoreExistingImports) { + configBuilder= ImportRewriteConfiguration.Builder.preservingOriginalImports(); + } else { + configBuilder= ImportRewriteConfiguration.Builder.discardingOriginalImports(); + } + + configBuilder.setImportOrder(Arrays.asList(this.importOrder)); + configBuilder.setTypeOnDemandThreshold(this.importOnDemandThreshold); + configBuilder.setStaticOnDemandThreshold(this.staticImportOnDemandThreshold); + + configBuilder.setTypeContainerSorting(this.useContextToFilterImplicitImports ? + ImportContainerSorting.BY_PACKAGE : ImportContainerSorting.BY_PACKAGE_AND_CONTAINING_TYPE); + + configBuilder.setStaticContainerSorting(ImportContainerSorting.BY_PACKAGE_AND_CONTAINING_TYPE); + + configBuilder.setImplicitImportIdentification(this.filterImplicitImports ? + ImplicitImportIdentification.JAVA_LANG_AND_CU_PACKAGE : ImplicitImportIdentification.NONE); + + return configBuilder.build(); } /** @@ -1219,24 +1277,23 @@ * @return boolean returns if any changes to imports have been recorded. */ public boolean hasRecordedChanges() { - return !this.restoreExistingImports || - (this.addedImports != null && !this.addedImports.isEmpty()) || - (this.removedImports != null && !this.removedImports.isEmpty()); + return !this.restoreExistingImports + || !this.addedImports.isEmpty() + || !this.removedImports.isEmpty(); } - private static String[] filterFromList(List imports, char prefix) { + private static String[] filterFromList(List imports, char prefix) { if (imports == null) { return CharOperation.NO_STRINGS; } - ArrayList res= new ArrayList(); - for (int i= 0; i < imports.size(); i++) { - String curr= (String) imports.get(i); + List res= new ArrayList(); + for (String curr : imports) { if (prefix == curr.charAt(0)) { res.add(curr.substring(1)); } } - return (String[]) res.toArray(new String[res.size()]); + return res.toArray(new String[res.size()]); } private void annotateList(List annotations, IAnnotationBinding [] annotationBindings, AST ast, ImportRewriteContext context) { diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/ImportRewriteAnalyzer.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/ImportRewriteAnalyzer.java deleted file mode 100644 index c83c7f2..0000000 --- a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/ImportRewriteAnalyzer.java +++ /dev/null @@ -1,1522 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2000, 2013 IBM Corporation and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * IBM Corporation - initial API and implementation - * Stephan Herrmann - Contribution for Bug 378024 - Ordering of comments between imports not preserved - *******************************************************************************/ -package org.eclipse.jdt.internal.core.dom.rewrite; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.NullProgressMonitor; -import org.eclipse.jdt.core.IBuffer; -import org.eclipse.jdt.core.ICompilationUnit; -import org.eclipse.jdt.core.IJavaElement; -import org.eclipse.jdt.core.JavaCore; -import org.eclipse.jdt.core.JavaModelException; -import org.eclipse.jdt.core.Signature; -import org.eclipse.jdt.core.compiler.CharOperation; -import org.eclipse.jdt.core.dom.ASTNode; -import org.eclipse.jdt.core.dom.Comment; -import org.eclipse.jdt.core.dom.CompilationUnit; -import org.eclipse.jdt.core.dom.ImportDeclaration; -import org.eclipse.jdt.core.dom.LineComment; -import org.eclipse.jdt.core.dom.PackageDeclaration; -import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants; -import org.eclipse.jdt.core.search.IJavaSearchConstants; -import org.eclipse.jdt.core.search.IJavaSearchScope; -import org.eclipse.jdt.core.search.SearchEngine; -import org.eclipse.jdt.core.search.TypeNameRequestor; -import org.eclipse.jdt.internal.core.JavaProject; -import org.eclipse.jface.text.IRegion; -import org.eclipse.jface.text.Region; -import org.eclipse.text.edits.DeleteEdit; -import org.eclipse.text.edits.InsertEdit; -import org.eclipse.text.edits.MultiTextEdit; - -@SuppressWarnings({ "rawtypes", "unchecked" }) -public final class ImportRewriteAnalyzer { - - private final ICompilationUnit compilationUnit; - private final ArrayList packageEntries; - - private final List importsCreated; - private final List staticImportsCreated; - - private final IRegion replaceRange; - - private final int importOnDemandThreshold; - private final int staticImportOnDemandThreshold; - - private boolean filterImplicitImports; - private boolean useContextToFilterImplicitImports; - private boolean findAmbiguousImports; - - private IRegion[] preserveExistingCommentsRanges; - - private int flags= 0; - - private static final int F_NEEDS_LEADING_DELIM= 2; - private static final int F_NEEDS_TRAILING_DELIM= 4; - - private static final String JAVA_LANG= "java.lang"; //$NON-NLS-1$ - - public ImportRewriteAnalyzer( - ICompilationUnit cu, - CompilationUnit root, - String[] importOrder, - int threshold, - int staticThreshold, - boolean restoreExistingImports, - boolean useContextToFilterImplicitImports) { - this.compilationUnit= cu; - this.importOnDemandThreshold= threshold; - this.staticImportOnDemandThreshold= staticThreshold; - this.useContextToFilterImplicitImports = useContextToFilterImplicitImports; - - this.filterImplicitImports= true; - this.findAmbiguousImports= true; //!restoreExistingImports; - - this.packageEntries= new ArrayList(20); - this.importsCreated= new ArrayList(); - this.staticImportsCreated= new ArrayList(); - this.flags= 0; - - this.replaceRange= evaluateReplaceRange(root); - if (restoreExistingImports) { - addExistingImports(root); - } else { - // collect all existing comments inside imports and concatenate them - this.preserveExistingCommentsRanges = retrieveExistingCommentsInImports(root); - } - - PackageEntry[] order= new PackageEntry[importOrder.length]; - for (int i= 0; i < order.length; i++) { - String curr= importOrder[i]; - if (curr.length() > 0 && curr.charAt(0) == '#') { - curr= curr.substring(1); - order[i]= new PackageEntry(curr, curr, true); // static import group - } else { - order[i]= new PackageEntry(curr, curr, false); // normal import group - } - } - - addPreferenceOrderHolders(order); - } - - private int getSpacesBetweenImportGroups() { - try { - int num= Integer.parseInt(this.compilationUnit.getJavaProject().getOption(DefaultCodeFormatterConstants.FORMATTER_BLANK_LINES_BETWEEN_IMPORT_GROUPS, true)); - if (num >= 0) - return num; - } catch (NumberFormatException e) { - // fall through - } - return 1; - } - - private boolean insertSpaceBeforeSemicolon() { - return JavaCore.INSERT.equals(this.compilationUnit.getJavaProject().getOption(DefaultCodeFormatterConstants.FORMATTER_INSERT_SPACE_BEFORE_SEMICOLON, true)); - } - - private void addPreferenceOrderHolders(PackageEntry[] preferenceOrder) { - if (this.packageEntries.isEmpty()) { - // all new: copy the elements - for (int i= 0; i < preferenceOrder.length; i++) { - this.packageEntries.add(preferenceOrder[i]); - } - } else { - // match the preference order entries to existing imports - // entries not found are appended after the last successfully matched entry - - PackageEntry[] lastAssigned= new PackageEntry[preferenceOrder.length]; - - // find an existing package entry that matches most - for (int k= 0; k < this.packageEntries.size(); k++) { - PackageEntry entry= (PackageEntry) this.packageEntries.get(k); - if (!entry.isComment()) { - String currName= entry.getName(); - int currNameLen= currName.length(); - int bestGroupIndex= -1; - int bestGroupLen= -1; - for (int i= 0; i < preferenceOrder.length; i++) { - boolean currPrevStatic= preferenceOrder[i].isStatic(); - if (currPrevStatic == entry.isStatic()) { - String currPrefEntry= preferenceOrder[i].getName(); - int currPrefLen= currPrefEntry.length(); - if (currName.startsWith(currPrefEntry) && currPrefLen >= bestGroupLen) { - if (currPrefLen == currNameLen || currName.charAt(currPrefLen) == '.') { - if (bestGroupIndex == -1 || currPrefLen > bestGroupLen) { - bestGroupLen= currPrefLen; - bestGroupIndex= i; - } - } - } - } - } - if (bestGroupIndex != -1) { - entry.setGroupID(preferenceOrder[bestGroupIndex].getName()); - lastAssigned[bestGroupIndex]= entry; // remember last entry - } - } - } - // fill in not-assigned categories, keep partial order - int currAppendIndex= 0; - for (int i= 0; i < lastAssigned.length; i++) { - PackageEntry entry= lastAssigned[i]; - if (entry == null) { - PackageEntry newEntry= preferenceOrder[i]; - if (currAppendIndex == 0 && !newEntry.isStatic()) { - currAppendIndex= getIndexAfterStatics(); - } - this.packageEntries.add(currAppendIndex, newEntry); - currAppendIndex++; - } else { - currAppendIndex= this.packageEntries.indexOf(entry) + 1; - } - } - } - } - - private String getQualifier(ImportDeclaration decl) { - String name = decl.getName().getFullyQualifiedName(); - /* - * If it's on demand import, return the fully qualified name. (e.g. pack1.Foo.* => pack.Foo, pack1.* => pack1) - * This is because we need to have pack1.Foo.* and pack1.Bar under different qualifier groups. - */ - if (decl.isOnDemand()) { - return name; - } - return getQualifier(name, decl.isStatic()); - } - - private String getQualifier(String name, boolean isStatic) { - // For static imports, return the Type name as well as part of the qualifier - if (isStatic || !this.useContextToFilterImplicitImports) { - return Signature.getQualifier(name); - } - - char[] searchedName = name.toCharArray(); - int index = name.length(); - /* Stop at the last fragment */ - JavaProject project = (JavaProject) this.compilationUnit.getJavaProject(); - do { - String testedName = new String(searchedName, 0, index); - IJavaElement fragment = null; - try { - fragment = project.findPackageFragment(testedName); - } catch (JavaModelException e) { - return name; - } - if (fragment != null) { - return testedName; - } - try { - fragment = project.findType(testedName); - } catch (JavaModelException e) { - return name; - } - if (fragment != null) { - index = CharOperation.lastIndexOf(Signature.C_DOT, searchedName, 0, index - 1); - } else { - // we use the heuristic that a name starting with a lowercase is a package name - index = CharOperation.lastIndexOf(Signature.C_DOT, searchedName, 0, index - 1); - if (Character.isLowerCase(searchedName[index + 1])) { - return testedName; - } - } - } while (index >= 0); - return name; - } - - private static String getFullName(ImportDeclaration decl) { - String name= decl.getName().getFullyQualifiedName(); - return decl.isOnDemand() ? name + ".*": name; //$NON-NLS-1$ - } - - private void addExistingImports(CompilationUnit root) { - List/*ImportDeclaration*/ decls= root.imports(); - if (decls.isEmpty()) { - return; - } - PackageEntry currPackage= null; - - ImportDeclaration curr= (ImportDeclaration) decls.get(0); - int currOffset= curr.getStartPosition(); - int currLength= curr.getLength(); - int currEndLine= root.getLineNumber(currOffset + currLength); - - for (int i= 1; i < decls.size(); i++) { - boolean isStatic= curr.isStatic(); - String name= getFullName(curr); - String packName= getQualifier(curr); - if (currPackage == null || currPackage.compareTo(packName, isStatic) != 0) { - currPackage= new PackageEntry(packName, null, isStatic); - this.packageEntries.add(currPackage); - } - - ImportDeclaration next= (ImportDeclaration) decls.get(i); - int nextOffset= next.getStartPosition(); - int nextLength= next.getLength(); - int nextOffsetLine= root.getLineNumber(nextOffset); - - int extendedStart = root.getExtendedStartPosition(curr); - int extendedLength = root.getExtendedLength(curr); - if (extendedStart < this.replaceRange.getOffset()) { - // don't touch the first comments before the start of import declarations - extendedLength -= (currOffset - extendedStart); - extendedStart = currOffset; - } - - // if next import is on a different line, modify the end position to the next line begin offset - int nextLineOffset = nextOffset; // offset at the start of next line. Next import may not start here - if (currEndLine < nextOffsetLine) { - currEndLine++; - nextLineOffset = root.getPosition(currEndLine, 0); - // There may be some leading comments (or line delimiters) before the next import. The start of those comments - // is not the real start offset of the next import. So don't change nextOffset - } - // retrieve preceding and trailing comments if any - IRegion rangeBefore = null; - IRegion rangeAfter = null; - - if (currOffset > extendedStart) { - rangeBefore = new Region(extendedStart, currOffset - extendedStart); - } - int currLen = curr.getLength(); - if (currLen < extendedLength - (currOffset - extendedStart)) { - int currEndOffset = currOffset + currLen; - int rangeBeforeLen = rangeBefore != null? rangeBefore.getLength() : 0; - rangeAfter = new Region(currEndOffset, extendedLength - rangeBeforeLen - currLen); - } - currPackage.add( - new ImportDeclEntry( - packName.length(), - name, - isStatic, - new Region(currOffset, nextLineOffset - currOffset), // should not include leading comments of next import, line delimiters, etc. - rangeBefore, - rangeAfter)); - currOffset= nextOffset; - curr= next; - - // add a comment entry for spacing between imports - if (currEndLine < nextOffsetLine) { - nextOffset= root.getPosition(nextOffsetLine, 0); - - int length = nextOffset - nextLineOffset; - if (length > 2) { // valid comment has at least two chars - currPackage.add(new ImportDeclEntry(packName.length(), null, false, new Region(nextLineOffset, length))); - } - - currOffset= nextOffset; - } - currEndLine= root.getLineNumber(nextOffset + nextLength); - } - - boolean isStatic= curr.isStatic(); - String name= getFullName(curr); - String packName= getQualifier(curr); - if (currPackage == null || currPackage.compareTo(packName, isStatic) != 0) { - currPackage= new PackageEntry(packName, null, isStatic); - this.packageEntries.add(currPackage); - } - int currStartOffset = curr.getStartPosition(); - int currLen = curr.getLength(); - int extendedStartOffset = root.getExtendedStartPosition(curr); - IRegion leadingComments = null; - IRegion allTrailingComments = null; - - if (currStartOffset > extendedStartOffset) { - leadingComments = new Region(extendedStartOffset, currOffset - extendedStartOffset); - } - int length= this.replaceRange.getOffset() + this.replaceRange.getLength() - currStartOffset; - int extendedLength = root.getExtendedLength(curr); - if (currLen < extendedLength - (currOffset - extendedStartOffset)) { - int currEndOffset = currOffset + currLen; - int leadingCommentsLen = leadingComments != null? leadingComments.getLength() : 0; - allTrailingComments = new Region(currEndOffset, extendedLength - leadingCommentsLen - currLen); - } - currPackage.add(new ImportDeclEntry(packName.length(), name, isStatic, new Region(curr.getStartPosition(), length), leadingComments, allTrailingComments)); - } - - private IRegion[] retrieveExistingCommentsInImports(CompilationUnit root) { - List/*ImportDeclaration*/ decls= root.imports(); - if (decls.isEmpty()) { - return null; - } - - List commentList = root.getCommentList(); - int numberOfComments = commentList.size(); - List regions = null; - int currentExtendedEnd = -1; - int currEndLine= -1; - - /* for the first comment, we only take the trailing comment if any and the replace range doesn't - * include the preceding comment - */ - for (int i= 0; i < decls.size(); i++) { - ImportDeclaration next= (ImportDeclaration) decls.get(i); - int nextOffset= next.getStartPosition(); - int nextLength= next.getLength(); - - int extendedStart = root.getExtendedStartPosition(next); - int extendedLength = root.getExtendedLength(next); - int nextOffsetLine= root.getLineNumber(nextOffset); - - if (nextOffset != extendedStart) { - // preceding comment - int lengthOfPrecedingComment = nextOffset - extendedStart; - if (i != 0) { - if (regions == null) { - regions = new ArrayList(); - } - regions.add(new Region(extendedStart, lengthOfPrecedingComment)); - } - - if (extendedLength != (nextLength + lengthOfPrecedingComment)) { - // Preceding and trailing comments - int regionLength = extendedLength - (nextLength + lengthOfPrecedingComment); - if (regions == null) { - regions = new ArrayList(); - } - regions.add(new Region(nextOffset + nextLength, regionLength)); - } - } else if (nextLength != extendedLength) { - // no extended start - only trailing comment - int regionLength = extendedLength - nextLength; - if (regions == null) { - regions = new ArrayList(); - } - regions.add(new Region(nextOffset + nextLength, regionLength)); - } - if (i > 0) { - // record comments between the previous comment and the current one that are not part - // of any comment extended range. - if ((nextOffsetLine - currEndLine) > 1) { - // check for comments between the two imports - LineComment comment = root.getAST().newLineComment(); - comment.setSourceRange(currentExtendedEnd + 1, 0); - int index = Collections.binarySearch(commentList, comment, new Comparator() { - public int compare(Object o1, Object o2) { - return ((Comment) o1).getStartPosition() - ((Comment) o2).getStartPosition(); - } - }); - // index = -(insertion point) - 1. - if (index < 0) { - loop: for (int j = -(index + 1); j < numberOfComments; j++) { - Comment currentComment = (Comment) commentList.get(j); - int commentStartPosition = currentComment.getStartPosition(); - int commentLength = currentComment.getLength(); - if ((commentStartPosition > currentExtendedEnd) - && ((commentStartPosition + commentLength - 1) < extendedStart)) { - if (regions == null) { - regions = new ArrayList(); - } - regions.add(new Region(commentStartPosition, commentLength)); - } else { - break loop; - } - } - } - } - } - currentExtendedEnd = extendedStart + extendedLength - 1; - currEndLine = root.getLineNumber(currentExtendedEnd); - } - if (regions == null) { - return null; - } - // sort regions according to their positions to restore comments in the same order - IRegion[] result = (IRegion[]) regions.toArray(new IRegion[regions.size()]); - Arrays.sort(result, new Comparator() { - public int compare(Object o1, Object o2) { - return ((IRegion) o1).getOffset() - ((IRegion) o2).getOffset(); - } - }); - return result; - } - /** - * Specifies that implicit imports (for types in java.lang, types in the same package as the rewrite - * compilation unit and types in the compilation unit's main type) should not be created, except if necessary to - * resolve an on-demand import conflict. - *

- * The filter is enabled by default. - *

- *

- * Note: {@link #ImportRewriteAnalyzer(ICompilationUnit, CompilationUnit, String[], int, int, boolean, boolean)} with true as the last - * parameter can be used to filter implicit imports when a context is used. - *

- * - * @param filterImplicitImports - * if true, implicit imports will be filtered - * - * @see #ImportRewriteAnalyzer(ICompilationUnit, CompilationUnit, String[], int, int, boolean, boolean) - */ - public void setFilterImplicitImports(boolean filterImplicitImports) { - this.filterImplicitImports= filterImplicitImports; - } - /** - * When set searches for imports that can not be folded into on-demand - * imports but must be specified explicitly - * @param findAmbiguousImports The new value - */ - public void setFindAmbiguousImports(boolean findAmbiguousImports) { - this.findAmbiguousImports= findAmbiguousImports; - } - - private static class PackageMatcher { - private String newName; - private String bestName; - private int bestMatchLen; - - public PackageMatcher() { - // initialization in 'initialize' - } - - public void initialize(String newImportName, String bestImportName) { - this.newName= newImportName; - this.bestName= bestImportName; - this.bestMatchLen= getCommonPrefixLength(bestImportName, newImportName); - } - - public boolean isBetterMatch(String currName, boolean preferCurr) { - boolean isBetter; - int currMatchLen= getCommonPrefixLength(currName, this.newName); - int matchDiff= currMatchLen - this.bestMatchLen; - if (matchDiff == 0) { - if (currMatchLen == this.newName.length() && currMatchLen == currName.length() && currMatchLen == this.bestName.length()) { - // duplicate entry and complete match - isBetter= preferCurr; - } else { - isBetter= sameMatchLenTest(currName); - } - } else { - isBetter= (matchDiff > 0); // curr has longer match - } - if (isBetter) { - this.bestName= currName; - this.bestMatchLen= currMatchLen; - } - return isBetter; - } - - private boolean sameMatchLenTest(String currName) { - int matchLen= this.bestMatchLen; - // known: bestName and currName differ from newName at position 'matchLen' - // currName and bestName don't have to differ at position 'matchLen' - - // determine the order and return true if currName is closer to newName - char newChar= getCharAt(this.newName, matchLen); - char currChar= getCharAt(currName, matchLen); - char bestChar= getCharAt(this.bestName, matchLen); - - if (newChar < currChar) { - if (bestChar < newChar) { // b < n < c - return (currChar - newChar) < (newChar - bestChar); // -> (c - n) < (n - b) - } else { // n < b && n < c - if (currChar == bestChar) { // longer match between curr and best - return false; // keep curr and best together, new should be before both - } else { - return currChar < bestChar; // -> (c < b) - } - } - } else { - if (bestChar > newChar) { // c < n < b - return (newChar - currChar) < (bestChar - newChar); // -> (n - c) < (b - n) - } else { // n > b && n > c - if (currChar == bestChar) { // longer match between curr and best - return true; // keep curr and best together, new should be ahead of both - } else { - return currChar > bestChar; // -> (c > b) - } - } - } - } - } - - /* package */ static int getCommonPrefixLength(String s, String t) { - int len= Math.min(s.length(), t.length()); - for (int i= 0; i < len; i++) { - if (s.charAt(i) != t.charAt(i)) { - return i; - } - } - return len; - } - - /* package */ static char getCharAt(String str, int index) { - if (str.length() > index) { - return str.charAt(index); - } - return 0; - } - - private PackageEntry findBestMatch(String newName, boolean isStatic) { - if (this.packageEntries.isEmpty()) { - return null; - } - String groupId= null; - int longestPrefix= -1; - PackageEntry matchingCommentEntry = null; - // find the matching group - for (int i= 0; i < this.packageEntries.size(); i++) { - PackageEntry curr= (PackageEntry) this.packageEntries.get(i); - if (isStatic == curr.isStatic()) { - String currGroup= curr.getGroupID(); - if (currGroup != null && newName.startsWith(currGroup)) { - int prefixLen= currGroup.length(); - if (prefixLen == newName.length() && !curr.isComment()) { - return curr; // perfect fit, use entry - } else if (curr.isComment()) { - matchingCommentEntry = curr; // may be the only fit if no actual import of this group is already present - continue; - } - if ((newName.charAt(prefixLen) == '.' || prefixLen == 0) && prefixLen > longestPrefix) { - longestPrefix= prefixLen; - groupId= currGroup; - } - } - } - } - if (matchingCommentEntry != null) { - return matchingCommentEntry; - } - PackageEntry bestMatch= null; - PackageMatcher matcher= new PackageMatcher(); - matcher.initialize(newName, ""); //$NON-NLS-1$ - for (int i= 0; i < this.packageEntries.size(); i++) { // find the best match with the same group - PackageEntry curr= (PackageEntry) this.packageEntries.get(i); - if (!curr.isComment() && curr.isStatic() == isStatic) { - if (groupId == null || groupId.equals(curr.getGroupID())) { - boolean preferrCurr= (bestMatch == null) || (curr.getNumberOfImports() > bestMatch.getNumberOfImports()); - if (matcher.isBetterMatch(curr.getName(), preferrCurr)) { - bestMatch= curr; - } - } - } - } - return bestMatch; - } - - private boolean isImplicitImport(String qualifier) { - if (JAVA_LANG.equals(qualifier)) { - return true; - } - ICompilationUnit cu= this.compilationUnit; - String packageName= cu.getParent().getElementName(); - if (qualifier.equals(packageName)) { - return true; - } - String mainTypeName= JavaCore.removeJavaLikeExtension(cu.getElementName()); - if (packageName.length() == 0) { - return qualifier.equals(mainTypeName); - } - return qualifier.equals(packageName +'.' + mainTypeName); - } - - public void addImport(String fullTypeName, boolean isStatic, CompilationUnit root, boolean restoreExistingImports) { - String typeContainerName= getQualifier(fullTypeName, isStatic); - ImportDeclEntry decl; - if (restoreExistingImports) { - decl = new ImportDeclEntry(typeContainerName.length(), fullTypeName, isStatic, null); - } else { - decl = addImportDeclEntry(typeContainerName, fullTypeName, isStatic, root); - } - sortIn(typeContainerName, decl, isStatic); - } - - /** - * adds the import entry, but if its an existing import entry then preserves the comments surrounding the import - */ - private ImportDeclEntry addImportDeclEntry(String containerName, String fullTypeName, boolean isStatic, CompilationUnit root) { - List/*ImportDeclaration*/ decls= root.imports(); - if (decls.isEmpty() || this.preserveExistingCommentsRanges == null || this.preserveExistingCommentsRanges.length == 0) { - return new ImportDeclEntry(containerName.length(), fullTypeName, isStatic, null); - } - IRegion precedingCommentRange = null; - IRegion trailingCommentRange = null; - int prevOffset = this.replaceRange.getOffset(); // will store offset of the previous import's extended end - int numOfImports = decls.size(); - for (int i= 0; i < numOfImports; i++) { - ImportDeclaration curr= (ImportDeclaration) decls.get(i); - int currOffset= curr.getStartPosition(); - int currLength= curr.getLength(); - int currExtendedStart = root.getExtendedStartPosition(curr); - int currExtendedLen = root.getExtendedLength(curr); - String name= getFullName(curr); - String packName= getQualifier(curr); - if (packName.equals(containerName) && (name.equals(fullTypeName) || name.endsWith("*"))) {//$NON-NLS-1$ - int preserveCommentsLen = this.preserveExistingCommentsRanges.length; - for (int j = 0; j < preserveCommentsLen; j++) { - int offset = this.preserveExistingCommentsRanges[j].getOffset(); - boolean wasRangeUsed = false; - int existingCommentLength = this.preserveExistingCommentsRanges[j].getLength(); - if (offset == currExtendedStart) { - // comments belonging to this import's extended start - precedingCommentRange = new Region(offset, existingCommentLength); - wasRangeUsed = true; - } else if (offset < currExtendedStart && offset > prevOffset) { - // comment between two imports but not inside either's extended ranges - // to preserve the position of these comments add a dummy comment entry - PackageEntry commentEntry = new PackageEntry(); // create a comment package entry for this - commentEntry.setGroupID(packName); // the comment should belong to the current group - this.packageEntries.add(commentEntry); - commentEntry.add(new ImportDeclEntry(packName.length(), null, false, new Region(offset, existingCommentLength))); - wasRangeUsed = true; - } else if ((currExtendedStart + currExtendedLen) != (currOffset + currLength)){ - if (offset == currOffset + currLength) { - // comment is in the extended end of the import - trailingCommentRange = new Region(offset, existingCommentLength); - wasRangeUsed = true; - } else if (offset > (currOffset + currLength)) { - break; - } - } - if (wasRangeUsed) { - // remove this comment from preserveExistingCommentsRanges array - IRegion[] tempRegions = new IRegion[--preserveCommentsLen]; - System.arraycopy(this.preserveExistingCommentsRanges, 0, tempRegions, 0, j); - System.arraycopy(this.preserveExistingCommentsRanges, j+1, tempRegions, j, tempRegions.length - j); - this.preserveExistingCommentsRanges = tempRegions; - j--; - } - } - return new ImportDeclEntry(containerName.length(), fullTypeName, isStatic, null, precedingCommentRange, trailingCommentRange); - } - prevOffset = currExtendedStart + currExtendedLen - 1; - } - return new ImportDeclEntry(containerName.length(), fullTypeName, isStatic, null); - } - - public boolean removeImport(String qualifiedName, boolean isStatic) { - String containerName= getQualifier(qualifiedName, isStatic); - - int nPackages= this.packageEntries.size(); - for (int i= 0; i < nPackages; i++) { - PackageEntry entry= (PackageEntry) this.packageEntries.get(i); - if (entry.compareTo(containerName, isStatic) == 0) { - if (entry.remove(qualifiedName, isStatic)) { - return true; - } - } - } - return false; - } - - private int getIndexAfterStatics() { - for (int i= 0; i < this.packageEntries.size(); i++) { - if (!((PackageEntry) this.packageEntries.get(i)).isStatic()) { - return i; - } - } - return this.packageEntries.size(); - } - - - private void sortIn(String typeContainerName, ImportDeclEntry decl, boolean isStatic) { - PackageEntry bestMatch= findBestMatch(typeContainerName, isStatic); - if (bestMatch == null) { - PackageEntry packEntry= new PackageEntry(typeContainerName, null, isStatic); - packEntry.add(decl); - int insertPos= packEntry.isStatic() ? 0 : getIndexAfterStatics(); - this.packageEntries.add(insertPos, packEntry); - } else { - int cmp= typeContainerName.compareTo(bestMatch.getName()); - if (cmp == 0) { - bestMatch.sortIn(decl); - } else { - // create a new package entry - String group= bestMatch.getGroupID(); - if (group != null) { - if (!typeContainerName.startsWith(group)) { - group= null; - } - } - PackageEntry packEntry= new PackageEntry(typeContainerName, group, isStatic); - packEntry.add(decl); - int index= this.packageEntries.indexOf(bestMatch); - if (cmp < 0) { - // insert ahead of best match - this.packageEntries.add(index, packEntry); - } else { - // insert after best match - this.packageEntries.add(index + 1, packEntry); - } - } - } - } - - private IRegion evaluateReplaceRange(CompilationUnit root) { - List imports= root.imports(); - if (!imports.isEmpty()) { - ImportDeclaration first= (ImportDeclaration) imports.get(0); - ImportDeclaration last= (ImportDeclaration) imports.get(imports.size() - 1); - - int startPos= first.getStartPosition(); // no extended range for first: bug 121428 - int endPos= root.getExtendedStartPosition(last) + root.getExtendedLength(last); - int endLine= root.getLineNumber(endPos); - if (endLine > 0) { - int nextLinePos= root.getPosition(endLine + 1, 0); - if (nextLinePos >= 0) { - int firstTypePos= getFirstTypeBeginPos(root); - if (firstTypePos != -1 && firstTypePos < nextLinePos) { - endPos= firstTypePos; - } else { - endPos= nextLinePos; - } - } - } - return new Region(startPos, endPos - startPos); - } else { - int start= getPackageStatementEndPos(root); - return new Region(start, 0); - } - } - - public MultiTextEdit getResultingEdits(IProgressMonitor monitor) throws JavaModelException { - if (monitor == null) { - monitor= new NullProgressMonitor(); - } - try { - int importsStart= this.replaceRange.getOffset(); - int importsLen= this.replaceRange.getLength(); - - String lineDelim= this.compilationUnit.findRecommendedLineSeparator(); - IBuffer buffer= this.compilationUnit.getBuffer(); - - int currPos= importsStart; - MultiTextEdit resEdit= new MultiTextEdit(); - - if ((this.flags & F_NEEDS_LEADING_DELIM) != 0) { - // new import container - resEdit.addChild(new InsertEdit(currPos, lineDelim)); - } - - PackageEntry lastPackage= null; - - Set onDemandConflicts= null; - if (this.findAmbiguousImports) { - onDemandConflicts= evaluateStarImportConflicts(monitor); - } - - int spacesBetweenGroups= getSpacesBetweenImportGroups(); - - ArrayList stringsToInsert= new ArrayList(); - - int nPackageEntries= this.packageEntries.size(); - for (int i= 0; i < nPackageEntries; i++) { - PackageEntry pack= (PackageEntry) this.packageEntries.get(i); - if (this.filterImplicitImports && !pack.isStatic() && isImplicitImport(pack.getName())) { - pack.filterImplicitImports(this.useContextToFilterImplicitImports); - } - int nImports= pack.getNumberOfImports(); - if (nImports == 0) { - continue; - } - - if (spacesBetweenGroups > 0 && lastPackage != null) { - // add a space between two different groups by looking at the two adjacent imports - if (!lastPackage.isComment() && !pack.isComment() && !pack.isSameGroup(lastPackage)) { - for (int k= spacesBetweenGroups; k > 0; k--) { - stringsToInsert.add(lineDelim); - } - } else if (lastPackage.isComment() && pack.isSameGroup(lastPackage)) { - // the last pack may be a dummy for a comment which doesn't belong to any extended range - stringsToInsert.add(lineDelim); - } - } - lastPackage= pack; - - boolean isStatic= pack.isStatic(); - int threshold= isStatic ? this.staticImportOnDemandThreshold : this.importOnDemandThreshold; - - boolean doStarImport= pack.hasStarImport(threshold, onDemandConflicts); - boolean allImportsAddedToStar = false; - if (doStarImport && (pack.find("*") == null)) { //$NON-NLS-1$ - String[] imports = getNewImportStrings(buffer, pack, isStatic, lineDelim); - for (int j = 0, max = imports.length; j < max; j++) { - stringsToInsert.add(imports[j]); - } - allImportsAddedToStar = true; // may still need to handle onDemandConflicts below - } - - for (int k= 0; k < nImports; k++) { - ImportDeclEntry currDecl= pack.getImportAt(k); - IRegion region= currDecl.getSourceRange(); - boolean isConflict = !currDecl.isComment() && onDemandConflicts != null && onDemandConflicts.contains(currDecl.getSimpleName()); - boolean addRegularToStar = doStarImport && !currDecl.isOnDemand(); - - if (region == null) { // new entry - if (!addRegularToStar || isConflict) { - IRegion rangeBefore = currDecl.getPrecedingCommentRange(); - IRegion rangeAfter = currDecl.getTrailingCommentRange(); - if (rangeBefore != null) { - stringsToInsert.add(buffer.getText(rangeBefore.getOffset(), rangeBefore.getLength())); - } - - String trailingComment = null; - if (rangeAfter != null) { - trailingComment = buffer.getText(rangeAfter.getOffset(), rangeAfter.getLength()); - } - String str= getNewImportString(currDecl.getElementName(), isStatic, trailingComment, lineDelim); - stringsToInsert.add(str); - } else if (addRegularToStar && !allImportsAddedToStar) { - String simpleName = currDecl.getTypeQualifiedName(); - if (simpleName.indexOf('.') != -1) { - String str= getNewImportString(currDecl.getElementName(), isStatic, lineDelim); - if (stringsToInsert.indexOf(str) == -1) { - stringsToInsert.add(str); - } - } - } - } else if (!addRegularToStar || isConflict) { - int offset= region.getOffset(); - IRegion rangeBefore = currDecl.getPrecedingCommentRange(); - if (rangeBefore != null && currPos > rangeBefore.getOffset()) { - // moved ahead of the leading comments, bring the currPos back - currPos = rangeBefore.getOffset(); - } - if (rangeBefore != null) { - stringsToInsert.add(buffer.getText(rangeBefore.getOffset(), rangeBefore.getLength())); - } - removeAndInsertNew(buffer, currPos, offset, stringsToInsert, resEdit); - stringsToInsert.clear(); - currPos= offset + region.getLength(); - } else if (addRegularToStar && !allImportsAddedToStar && !currDecl.isComment()) { - String simpleName = currDecl.getTypeQualifiedName(); - if (simpleName.indexOf('.') != -1) { - IRegion rangeBefore = currDecl.getPrecedingCommentRange(); - if (rangeBefore != null && currPos > rangeBefore.getOffset()) { - // moved ahead of the leading comments, bring the currPos back - currPos = rangeBefore.getOffset(); - } - if (rangeBefore != null) { - stringsToInsert.add(buffer.getText(rangeBefore.getOffset(), rangeBefore.getLength())); - } - IRegion rangeAfter = currDecl.getTrailingCommentRange(); - String trailingComment = null; - if (rangeAfter != null) { - trailingComment = buffer.getText(rangeAfter.getOffset(), rangeAfter.getLength()); - } - String str= getNewImportString(currDecl.getElementName(), isStatic, trailingComment, lineDelim); - if (stringsToInsert.indexOf(str) == -1) { - stringsToInsert.add(str); - } - } - } - } - } - - // insert back all existing imports comments since existing imports were not preserved - if (this.preserveExistingCommentsRanges != null) { - for (int i = 0, max = this.preserveExistingCommentsRanges.length; (i < max && this.preserveExistingCommentsRanges[i] != null); i++) { - IRegion region = this.preserveExistingCommentsRanges[i]; - String text = buffer.getText(region.getOffset(), region.getLength()); - // remove preceding whitespaces - int index = 0; - int length = text.length(); - loop: while (index < length) { - if (Character.isWhitespace(text.charAt(index))) { - index++; - } else { - break loop; - } - } - if (index != 0) { - text = text.substring(index); - } - if (!text.endsWith(lineDelim)) { - text += lineDelim; - } - stringsToInsert.add(text); - } - } - int end= importsStart + importsLen; - removeAndInsertNew(buffer, currPos, end, stringsToInsert, resEdit); - - if (importsLen == 0) { - if (!this.importsCreated.isEmpty() || !this.staticImportsCreated.isEmpty()) { // new import container - if ((this.flags & F_NEEDS_TRAILING_DELIM) != 0) { - resEdit.addChild(new InsertEdit(currPos, lineDelim)); - } - } else { - return new MultiTextEdit(); // no changes - } - } - return resEdit; - } finally { - monitor.done(); - } - } - - private void removeAndInsertNew(IBuffer buffer, int contentOffset, int contentEnd, ArrayList stringsToInsert, MultiTextEdit resEdit) { - int pos= contentOffset; - for (int i= 0; i < stringsToInsert.size(); i++) { - String curr= (String) stringsToInsert.get(i); - int idx= findInBuffer(buffer, curr, pos, contentEnd); - if (idx != -1) { - if (idx != pos) { - resEdit.addChild(new DeleteEdit(pos, idx - pos)); - } - pos= idx + curr.length(); - } else { - resEdit.addChild(new InsertEdit(pos, curr)); - } - } - if (pos < contentEnd) { - resEdit.addChild(new DeleteEdit(pos, contentEnd - pos)); - } - } - - private int findInBuffer(IBuffer buffer, String str, int start, int end) { - int pos= start; - int len= str.length(); - if (pos + len > end || str.length() == 0) { - return -1; - } - char first= str.charAt(0); - int step= str.indexOf(first, 1); - if (step == -1) { - step= len; - } - while (pos + len <= end) { - if (buffer.getChar(pos) == first) { - int k= 1; - while (k < len && buffer.getChar(pos + k) == str.charAt(k)) { - k++; - } - if (k == len) { - return pos; // found - } - if (k < step) { - pos+= k; - } else { - pos+= step; - } - } else { - pos++; - } - } - return -1; - } - - private Set evaluateStarImportConflicts(IProgressMonitor monitor) throws JavaModelException { - //long start= System.currentTimeMillis(); - - final HashSet/*String*/ onDemandConflicts= new HashSet(); - - IJavaSearchScope scope= SearchEngine.createJavaSearchScope(new IJavaElement[] { this.compilationUnit.getJavaProject() }); - - ArrayList/**/ starImportPackages= new ArrayList(); - ArrayList/**/ simpleTypeNames= new ArrayList(); - int nPackageEntries= this.packageEntries.size(); - for (int i= 0; i < nPackageEntries; i++) { - PackageEntry pack= (PackageEntry) this.packageEntries.get(i); - if (!pack.isStatic() && pack.hasStarImport(this.importOnDemandThreshold, null)) { - starImportPackages.add(pack.getName().toCharArray()); - for (int k= 0; k < pack.getNumberOfImports(); k++) { - ImportDeclEntry curr= pack.getImportAt(k); - if (!curr.isOnDemand() && !curr.isComment()) { - simpleTypeNames.add(curr.getSimpleName().toCharArray()); - } - } - } - } - if (starImportPackages.isEmpty()) { - return null; - } - - starImportPackages.add(this.compilationUnit.getParent().getElementName().toCharArray()); - starImportPackages.add(JAVA_LANG.toCharArray()); - - char[][] allPackages= (char[][]) starImportPackages.toArray(new char[starImportPackages.size()][]); - char[][] allTypes= (char[][]) simpleTypeNames.toArray(new char[simpleTypeNames.size()][]); - - TypeNameRequestor requestor= new TypeNameRequestor() { - HashMap foundTypes= new HashMap(); - - private String getTypeContainerName(char[] packageName, char[][] enclosingTypeNames) { - StringBuffer buf= new StringBuffer(); - buf.append(packageName); - for (int i= 0; i < enclosingTypeNames.length; i++) { - if (buf.length() > 0) - buf.append('.'); - buf.append(enclosingTypeNames[i]); - } - return buf.toString(); - } - - public void acceptType(int modifiers, char[] packageName, char[] simpleTypeName, char[][] enclosingTypeNames, String path) { - String name= new String(simpleTypeName); - String containerName= getTypeContainerName(packageName, enclosingTypeNames); - - String oldContainer= (String) this.foundTypes.put(name, containerName); - if (oldContainer != null && !oldContainer.equals(containerName)) { - onDemandConflicts.add(name); - } - } - }; - new SearchEngine().searchAllTypeNames(allPackages, allTypes, scope, requestor, IJavaSearchConstants.WAIT_UNTIL_READY_TO_SEARCH, monitor); - return onDemandConflicts; - } - - private String getNewImportString(String importName, boolean isStatic, String lineDelim) { - return getNewImportString(importName, isStatic, null, lineDelim); - } - - private String getNewImportString(String importName, boolean isStatic, String trailingComment, String lineDelim) { - StringBuffer buf= new StringBuffer(); - buf.append("import "); //$NON-NLS-1$ - if (isStatic) { - buf.append("static "); //$NON-NLS-1$ - } - buf.append(importName); - if (insertSpaceBeforeSemicolon()) buf.append(' '); - buf.append(';'); - if (trailingComment != null) { - buf.append(trailingComment); - } - buf.append(lineDelim); - - if (isStatic) { - this.staticImportsCreated.add(importName); - } else { - this.importsCreated.add(importName); - } - return buf.toString(); - } - - private String[] getNewImportStrings(IBuffer buffer, PackageEntry packageEntry, boolean isStatic, String lineDelim) { - boolean isStarImportAdded = false; - List allImports = new ArrayList(); - int nImports = packageEntry.getNumberOfImports(); - StringBuffer allComments = null; - StringBuffer allCommentsLead = null; - for (int i= 0; i < nImports; i++) { - ImportDeclEntry curr= packageEntry.getImportAt(i); - if (curr.isComment()) { - IRegion rangeBefore = curr.getPrecedingCommentRange(); - if (rangeBefore != null) { - allImports.add(buffer.getText(rangeBefore.getOffset(), rangeBefore.getLength())); - } - IRegion rangeAfter = curr.getTrailingCommentRange(); - String trailingComment = null; - if (rangeAfter != null) { - trailingComment = buffer.getText(rangeAfter.getOffset(), rangeAfter.getLength()); - } - if (trailingComment != null) { - allImports.add(buffer.getText(rangeAfter.getOffset(), rangeAfter.getLength())); - } - } else { - String simpleName = curr.getTypeQualifiedName(); - if (simpleName.indexOf('.') != -1) { - // member type imports - we preserve it - IRegion rangeBefore = curr.getPrecedingCommentRange(); - if (rangeBefore != null) { - allImports.add(buffer.getText(rangeBefore.getOffset(), rangeBefore.getLength())); - } - IRegion rangeAfter = curr.getTrailingCommentRange(); - String trailingComment = null; - if (rangeAfter != null) { - trailingComment = buffer.getText(rangeAfter.getOffset(), rangeAfter.getLength()); - } - allImports.add(getNewImportString(curr.getElementName(), isStatic, trailingComment, lineDelim)); - } else if (!isStarImportAdded) { - String starImportString= packageEntry.getName() + ".*"; //$NON-NLS-1$ - // collect all comments - IRegion rangeBefore = curr.getPrecedingCommentRange(); - if (rangeBefore != null) { - allImports.add(buffer.getText(rangeBefore.getOffset(), rangeBefore.getLength())); - } - IRegion rangeAfter = curr.getTrailingCommentRange(); - String trailComments = null; - if (rangeAfter != null) { - trailComments = buffer.getText(rangeAfter.getOffset(), rangeAfter.getLength()); - } - allImports.add(getNewImportString(starImportString, isStatic, trailComments, lineDelim)); - isStarImportAdded = true; - } else { - // collect all comments - IRegion rangeBefore = curr.getPrecedingCommentRange(); - if (rangeBefore != null) { - if (allCommentsLead == null) { - allCommentsLead = new StringBuffer(); - } - allCommentsLead.append(buffer.getText(rangeBefore.getOffset(), rangeBefore.getLength())); - } - IRegion rangeAfter = curr.getTrailingCommentRange(); - if (rangeAfter != null) { - if (allComments == null) { - allComments = new StringBuffer(); - } - allComments.append(buffer.getText(rangeAfter.getOffset(), rangeAfter.getLength())); - } - } - } - } - if (allCommentsLead != null) { - allImports.add(0, String.valueOf(allCommentsLead)); - } - if (allComments != null) { - allImports.add(String.valueOf(allComments.append(lineDelim))); - } - return (String[]) allImports.toArray(new String[allImports.size()]); - } - - private static int getFirstTypeBeginPos(CompilationUnit root) { - List types= root.types(); - if (!types.isEmpty()) { - return root.getExtendedStartPosition(((ASTNode) types.get(0))); - } - return -1; - } - - private int getPackageStatementEndPos(CompilationUnit root) { - PackageDeclaration packDecl= root.getPackage(); - if (packDecl != null) { - int afterPackageStatementPos= -1; - int lineNumber= root.getLineNumber(packDecl.getStartPosition() + packDecl.getLength()); - if (lineNumber >= 0) { - int lineAfterPackage= lineNumber + 1; - afterPackageStatementPos= root.getPosition(lineAfterPackage, 0); - } - if (afterPackageStatementPos < 0) { - this.flags|= F_NEEDS_LEADING_DELIM; - return packDecl.getStartPosition() + packDecl.getLength(); - } - int firstTypePos= getFirstTypeBeginPos(root); - if (firstTypePos != -1 && firstTypePos <= afterPackageStatementPos) { - this.flags|= F_NEEDS_TRAILING_DELIM; - if (firstTypePos == afterPackageStatementPos) { - this.flags|= F_NEEDS_LEADING_DELIM; - } - return firstTypePos; - } - this.flags|= F_NEEDS_LEADING_DELIM; - return afterPackageStatementPos; // insert a line after after package statement - } - this.flags |= F_NEEDS_TRAILING_DELIM; - return 0; - } - - public String toString() { - int nPackages= this.packageEntries.size(); - StringBuffer buf= new StringBuffer("\n-----------------------\n"); //$NON-NLS-1$ - for (int i= 0; i < nPackages; i++) { - PackageEntry entry= (PackageEntry) this.packageEntries.get(i); - if (entry.isStatic()) { - buf.append("static "); //$NON-NLS-1$ - } - buf.append(entry.toString()); - } - return buf.toString(); - } - - private static final class ImportDeclEntry { - - private String elementName; - private IRegion sourceRange; - private final boolean isStatic; - private int containerNameLength; - IRegion precedingCommentRange; - IRegion trailingCommentRange; - - public ImportDeclEntry( - int containerNameLength, - String elementName, - boolean isStatic, - IRegion sourceRange, - IRegion precedingCommentRange, - IRegion trailingCommentRange) { - this(containerNameLength, elementName, isStatic, sourceRange); - this.precedingCommentRange = precedingCommentRange; - this.trailingCommentRange = trailingCommentRange; - } - - public ImportDeclEntry(int containerNameLength, String elementName, boolean isStatic, IRegion sourceRange) { - this.elementName= elementName; - this.sourceRange= sourceRange; - this.isStatic= isStatic; - this.containerNameLength = containerNameLength; - } - - public String getElementName() { - return this.elementName; - } - - public int compareTo(String fullName, boolean isStaticImport) { - int cmp= this.elementName.compareTo(fullName); - if (cmp == 0) { - if (this.isStatic == isStaticImport) { - return 0; - } - return this.isStatic ? -1 : 1; - } - return cmp; - } - - public String getSimpleName() { - return Signature.getSimpleName(this.elementName); - } - - public String getTypeQualifiedName() { - return this.elementName.substring(this.containerNameLength + 1); - } - - public boolean isOnDemand() { - return this.elementName != null && this.elementName.endsWith(".*"); //$NON-NLS-1$ - } - - public boolean isStatic() { - return this.isStatic; - } - - public boolean isNew() { - return this.sourceRange == null; - } - - public boolean isComment() { - return this.elementName == null; - } - - public IRegion getSourceRange() { - return this.sourceRange; - } - - public IRegion getPrecedingCommentRange() { - return this.precedingCommentRange; - } - - public IRegion getTrailingCommentRange() { - return this.trailingCommentRange; - } - } - - /* - * Internal element for the import structure: A container for imports - * of all types from the same package - */ - private final static class PackageEntry { - private String name; - private ArrayList importEntries; - private String group; - private boolean isStatic; - - /** - * Comment package entry - */ - public PackageEntry() { - this("!", null, false); //$NON-NLS-1$ - } - - /** - * @param name Name of the package entry. e.g. org.eclipse.jdt.ui, containing imports like - * org.eclipse.jdt.ui.JavaUI. - * @param group The index of the preference order entry assigned - * different group id's will result in spacers between the entries - */ - public PackageEntry(String name, String group, boolean isStatic) { - this.name= name; - this.importEntries= new ArrayList(5); - this.group= group; - this.isStatic= isStatic; - } - - public boolean isStatic() { - return this.isStatic; - } - - public int compareTo(String otherName, boolean isOtherStatic) { - int cmp= this.name.compareTo(otherName); - if (cmp == 0) { - if (this.isStatic == isOtherStatic) { - return 0; - } - return this.isStatic ? -1 : 1; - } - return cmp; - } - - public void sortIn(ImportDeclEntry imp) { - String fullImportName= imp.getElementName(); - int insertPosition= -1; - int nInports= this.importEntries.size(); - for (int i= 0; i < nInports; i++) { - ImportDeclEntry curr= getImportAt(i); - if (!curr.isComment()) { - int cmp= curr.compareTo(fullImportName, imp.isStatic()); - if (cmp == 0) { - return; // exists already - } else if (cmp > 0 && insertPosition == -1) { - insertPosition= i; - } - } - } - if (insertPosition == -1) { - this.importEntries.add(imp); - } else { - this.importEntries.add(insertPosition, imp); - } - } - - - public void add(ImportDeclEntry imp) { - this.importEntries.add(imp); - } - - public ImportDeclEntry find(String simpleName) { - int nInports= this.importEntries.size(); - for (int i= 0; i < nInports; i++) { - ImportDeclEntry curr= getImportAt(i); - if (!curr.isComment()) { - String currName= curr.getElementName(); - if (currName.endsWith(simpleName)) { - int dotPos= currName.length() - simpleName.length() - 1; - if ((dotPos == -1) || (dotPos > 0 && currName.charAt(dotPos) == '.')) { - return curr; - } - } - } - } - return null; - } - - public boolean remove(String fullName, boolean isStaticImport) { - int nInports= this.importEntries.size(); - for (int i= 0; i < nInports; i++) { - ImportDeclEntry curr= getImportAt(i); - if (!curr.isComment() && curr.compareTo(fullName, isStaticImport) == 0) { - this.importEntries.remove(i); - return true; - } - } - return false; - } - - public void filterImplicitImports(boolean useContextToFilterImplicitImports) { - int nInports= this.importEntries.size(); - for (int i= nInports - 1; i >= 0; i--) { - ImportDeclEntry curr= getImportAt(i); - if (curr.isNew()) { - if (!useContextToFilterImplicitImports) { - this.importEntries.remove(i); - } else { - String elementName = curr.getElementName(); - int lastIndexOf = elementName.lastIndexOf('.'); - boolean internalClassImport = lastIndexOf > getName().length(); - if (!internalClassImport) { - this.importEntries.remove(i); - } - } - } - } - } - - public ImportDeclEntry getImportAt(int index) { - return (ImportDeclEntry) this.importEntries.get(index); - } - - public boolean hasStarImport(int threshold, Set explicitImports) { - if (isComment() || isDefaultPackage()) { // can not star import default package - return false; - } - int nImports= getNumberOfImports(); - int count= 0; - boolean containsNew= false; - for (int i= 0; i < nImports; i++) { - ImportDeclEntry curr= getImportAt(i); - if (curr.isOnDemand()) { - return true; - } - if (!curr.isComment()) { - count++; - boolean isExplicit= !curr.isStatic() && (explicitImports != null) && explicitImports.contains(curr.getSimpleName()); - containsNew |= curr.isNew() && !isExplicit; - } - } - return (count >= threshold) && containsNew; - } - - public int getNumberOfImports() { - return this.importEntries.size(); - } - - public String getName() { - return this.name; - } - - public String getGroupID() { - return this.group; - } - - public void setGroupID(String groupID) { - this.group= groupID; - } - - public boolean isSameGroup(PackageEntry other) { - if (this.group == null) { - return other.getGroupID() == null; - } else { - return this.group.equals(other.getGroupID()) && (this.isStatic == other.isStatic()); - } - } - - public boolean isComment() { - return "!".equals(this.name); //$NON-NLS-1$ - } - - public boolean isDefaultPackage() { - return this.name.length() == 0; - } - - public String toString() { - StringBuffer buf= new StringBuffer(); - if (isComment()) { - buf.append("comment\n"); //$NON-NLS-1$ - } else { - buf.append(this.name); buf.append(", groupId: "); buf.append(this.group); buf.append("\n"); //$NON-NLS-1$ //$NON-NLS-2$ - int nImports= getNumberOfImports(); - for (int i= 0; i < nImports; i++) { - ImportDeclEntry curr= getImportAt(i); - buf.append(" "); //$NON-NLS-1$ - if (curr.isComment()) { - buf.append("comment"); //$NON-NLS-1$ - } else { - if (curr.isStatic()) { - buf.append("static "); //$NON-NLS-1$ - } - buf.append(curr.getTypeQualifiedName()); - } - if (curr.isNew()) { - buf.append(" (new)"); //$NON-NLS-1$ - } - buf.append("\n"); //$NON-NLS-1$ - } - } - return buf.toString(); - } - } - - public String[] getCreatedImports() { - return (String[]) this.importsCreated.toArray(new String[this.importsCreated.size()]); - } - - public String[] getCreatedStaticImports() { - return (String[]) this.staticImportsCreated.toArray(new String[this.staticImportsCreated.size()]); - } - -} diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ConflictIdentifier.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ConflictIdentifier.java new file mode 100644 index 0000000..a5bcb07 --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ConflictIdentifier.java @@ -0,0 +1,158 @@ +/******************************************************************************* + * Copyright (c) 2014 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.JavaModelException; + +public class ConflictIdentifier { + /** + * Encapsulates those simple names (of type imports and of static imports) which would be + * imported from multiple on-demand or implicit import containers. + */ + static final class Conflicts { + final Set typeConflicts; + final Set staticConflicts; + + Conflicts(Set typeConflicts, Set staticConflicts) { + this.typeConflicts = Collections.unmodifiableSet(new HashSet(typeConflicts)); + this.staticConflicts = Collections.unmodifiableSet(new HashSet(staticConflicts)); + } + + @Override + public String toString() { + return String.format( + "Conflicts(type: %s; static: %s)", this.typeConflicts, this.staticConflicts); //$NON-NLS-1$ + } + } + + private final OnDemandComputer onDemandComputer; + private final TypeConflictingSimpleNameFinder typeConflictFinder; + private final StaticConflictingSimpleNameFinder staticConflictFinder; + private final Set implicitImportContainers; + + ConflictIdentifier( + OnDemandComputer onDemandComputer, + TypeConflictingSimpleNameFinder typeConflictFinder, + StaticConflictingSimpleNameFinder staticConflictFinder, + Set implicitImportContainers) { + this.onDemandComputer = onDemandComputer; + this.typeConflictFinder = typeConflictFinder; + this.staticConflictFinder = staticConflictFinder; + this.implicitImportContainers = implicitImportContainers; + } + + /** + * Identifies the simple names (of the elements of {@code imports}) which would be imported from + * multiple on-demand or implicit import containers. + * + * @param imports + * imports whose simple names are to be considered for conflicts + * @param addedImports + * imports which have been added as part of the rewrite (and could therefore trigger + * on-demand reductions; a subset of {@code imports} + * @param typeExplicitSimpleNames + * simple names of types which are already known to require explicit imports + * @param staticExplicitSimpleNames + * simple names of statics which are already known to require explicit imports + * @param progressMonitor + * a progress monitor used to track time spent searching for conflicts + * @return a {@link Conflicts} object encapsulating the found conflicting type and static names + * @throws JavaModelException if an error occurs while searching for declarations + */ + Conflicts identifyConflicts( + Set imports, + Set addedImports, + Set typeExplicitSimpleNames, + Set staticExplicitSimpleNames, + IProgressMonitor progressMonitor) throws JavaModelException { + Collection onDemandCandidates = this.onDemandComputer.identifyPossibleReductions( + imports, addedImports, typeExplicitSimpleNames, staticExplicitSimpleNames); + + Set typeOnDemandContainers = new HashSet(extractContainerNames(onDemandCandidates, false)); + Set staticOnDemandContainers = new HashSet(extractContainerNames(onDemandCandidates, true)); + + if (!typeOnDemandContainers.isEmpty()) { + // Existing on-demands might conflict with new or existing on-demands. + typeOnDemandContainers.addAll(extractOnDemandContainerNames(imports, false)); + + // Implicitly imported types might conflict with type on-demands. + typeOnDemandContainers.addAll(this.implicitImportContainers); + + // Member types imported by static on-demands might conflict with type on-demands. + typeOnDemandContainers.addAll(staticOnDemandContainers); + } + + if (!staticOnDemandContainers.isEmpty()) { + // Existing on-demands might conflict with new or existing on-demands. + staticOnDemandContainers.addAll(extractOnDemandContainerNames(imports, true)); + } + + Set typeConflicts = findConflictingSimpleNames( + this.typeConflictFinder, imports, false, typeOnDemandContainers, progressMonitor); + + Set staticConflicts = findConflictingSimpleNames( + this.staticConflictFinder, imports, true, staticOnDemandContainers, progressMonitor); + + return new Conflicts(typeConflicts, staticConflicts); + } + + private Collection extractContainerNames( + Collection onDemandCandidates, boolean isStatic) { + Collection containerNames = new ArrayList(onDemandCandidates.size()); + for (OnDemandReduction onDemandCandidate : onDemandCandidates) { + ImportName containerOnDemand = onDemandCandidate.containerOnDemand; + if (containerOnDemand.isStatic == isStatic) { + containerNames.add(containerOnDemand.containerName); + } + } + + return containerNames; + } + + private Collection extractOnDemandContainerNames( + Collection imports, boolean isStatic) { + Collection onDemandContainerNames = new ArrayList(imports.size()); + for (ImportName importName : imports) { + if (importName.isOnDemand() && importName.isStatic == isStatic) { + onDemandContainerNames.add(importName.containerName); + } + } + + return onDemandContainerNames; + } + + private Set findConflictingSimpleNames( + ConflictingSimpleNameFinder conflictFinder, + Set imports, + boolean isStatic, + Set onDemandImportedContainers, + IProgressMonitor monitor) throws JavaModelException { + if (onDemandImportedContainers.isEmpty() || imports.isEmpty()) { + return Collections.emptySet(); + } + + Set simpleNames = new HashSet(); + for (ImportName currentImport : imports) { + if (currentImport.isStatic == isStatic) { + simpleNames.add(currentImport.simpleName); + } + } + + return conflictFinder.findConflictingSimpleNames(simpleNames, onDemandImportedContainers, monitor); + } +} diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ConflictingSimpleNameFinder.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ConflictingSimpleNameFinder.java new file mode 100644 index 0000000..f81235e --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ConflictingSimpleNameFinder.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2014 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.JavaModelException; + +import java.util.Set; + +interface ConflictingSimpleNameFinder { + /** + * Finds duplicate declarations of the specified simple names within the specified on-demand and + * implicit import containers. + * + * @param simpleNames + * simple names of single imports in the compilation unit + * @param onDemandAndImplicitContainerNames + * names of on-demand and implicitly imported containers (e.g. "java.lang") + * @param monitor + * a progress monitor used to track time spent searching for conflicts + */ + Set findConflictingSimpleNames( + Set simpleNames, + Set onDemandAndImplicitContainerNames, + IProgressMonitor monitor) throws JavaModelException; +} \ No newline at end of file diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportAdder.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportAdder.java new file mode 100644 index 0000000..9d25802 --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportAdder.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) 2014 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import java.util.Collection; +import java.util.List; + +interface ImportAdder { + /** + * Returns a new list containing the elements of {@code existingImports} and also containing + * each element of {@code importsToAdd} for which {@code existingImports} does not contain an + * equal element. + */ + List addImports(Collection existingImports, Collection importsToAdd); +} diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportComment.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportComment.java new file mode 100644 index 0000000..2c7d35b --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportComment.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2014 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import org.eclipse.jface.text.IRegion; + +final class ImportComment { + /** The original location of this comment in the compilation unit. */ + final IRegion region; + + /** + * The number of line delimiters following this comment and preceding the next comment or the + * associated import declaration. Used to preserve blank lines between comments and/or import + * declarations. Will be 0 for a trailing comment. + */ + final int succeedingLineDelimiters; + + ImportComment(IRegion region, int succeedingLineDelims) { + this.region = region; + this.succeedingLineDelimiters = succeedingLineDelims; + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportComparator.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportComparator.java new file mode 100644 index 0000000..d3288bc --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportComparator.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2014 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import java.util.Comparator; + +/** + * Sorts imports by, in order of decreasing precedence, the following: + *
    + *
  • configured import group order
  • + *
  • package name and/or qualified name of containing type (lexicographically)
  • + *
  • qualified name of import (lexicographically)
  • + *
+ */ +final class ImportComparator implements Comparator { + private static Comparator createQualifiedNameComparator() { + return new Comparator() { + @Override + public int compare(ImportName o1, ImportName o2) { + return o1.qualifiedName.compareTo(o2.qualifiedName); + } + }; + } + + private final Comparator importGroupComparator; + private final Comparator typeContainerComparator; + private final Comparator staticContainerComparator; + private final Comparator qualifiedNameComparator; + + ImportComparator( + ImportGroupComparator importGroupComparator, + Comparator typeContainerComparator, + Comparator staticContainerComparator) { + this.importGroupComparator = importGroupComparator; + this.typeContainerComparator = typeContainerComparator; + this.staticContainerComparator = staticContainerComparator; + this.qualifiedNameComparator = createQualifiedNameComparator(); + } + + @Override + public int compare(ImportName o1, ImportName o2) { + final int comparison; + + int importGroupComparison = this.importGroupComparator.compare(o1, o2); + if (importGroupComparison != 0) { + comparison = importGroupComparison; + } else { + // The two imports sorted into the same import group, so o2.isStatic == o1.isStatic. + Comparator containerComparator = + o1.isStatic ? this.staticContainerComparator : this.typeContainerComparator; + + int containerComparison = containerComparator.compare(o1, o2); + if (containerComparison != 0) { + comparison = containerComparison; + } else { + comparison = this.qualifiedNameComparator.compare(o1, o2); + } + } + + return comparison; + } +} diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportDeclarationWriter.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportDeclarationWriter.java new file mode 100644 index 0000000..6238374 --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportDeclarationWriter.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +final class ImportDeclarationWriter { + private final boolean insertSpaceBeforeSemicolon; + + ImportDeclarationWriter(boolean insertSpaceBeforeSemicolon) { + this.insertSpaceBeforeSemicolon = insertSpaceBeforeSemicolon; + } + + /** + * Writes the Java source for an import declaration of the given name. + */ + String writeImportDeclaration(ImportName importName) { + StringBuilder sb = new StringBuilder(); + + sb.append("import "); //$NON-NLS-1$ + + if (importName.isStatic) { + sb.append("static "); //$NON-NLS-1$ + } + + sb.append(importName.qualifiedName); + + if (this.insertSpaceBeforeSemicolon) { + sb.append(' '); + } + + sb.append(';'); + + return sb.toString(); + } +} diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportEditor.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportEditor.java new file mode 100644 index 0000000..144de49 --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportEditor.java @@ -0,0 +1,536 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.eclipse.jface.text.IRegion; +import org.eclipse.text.edits.DeleteEdit; +import org.eclipse.text.edits.InsertEdit; +import org.eclipse.text.edits.MoveSourceEdit; +import org.eclipse.text.edits.MoveTargetEdit; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.RangeMarker; +import org.eclipse.text.edits.ReplaceEdit; +import org.eclipse.text.edits.TextEdit; + +/** + * Creates TextEdits to apply changes to the order of import declarations to a compilation unit. + */ +final class ImportEditor { + /** + * Iterates through the compilation unit's original import order, providing in turn each + * original import and the original start position of that import's leading delimiter. + */ + private static final class OriginalImportsCursor { + private final Iterator originalImportIterator; + OriginalImportEntry currentOriginalImport; + int currentPosition; + + OriginalImportsCursor(int startPosition, Collection originalImportEntries) { + this.originalImportIterator = originalImportEntries.iterator(); + this.currentPosition = startPosition; + this.currentOriginalImport = + this.originalImportIterator.hasNext() ? this.originalImportIterator.next() : null; + } + + /** + * Advances this cursor to the next import in the original order. + */ + void advance() { + IRegion declarationAndComments = this.currentOriginalImport.declarationAndComments; + this.currentPosition = declarationAndComments.getOffset() + declarationAndComments.getLength(); + this.currentOriginalImport = + this.originalImportIterator.hasNext() ? this.originalImportIterator.next() : null; + } + } + + private static final class ImportEdits { + final Collection leadingDelimiterEdits; + final Collection commentAndDeclarationEdits; + + ImportEdits( + Collection leadingDelimiterEdits, + Collection commentAndDeclarationEdits) { + this.leadingDelimiterEdits = leadingDelimiterEdits; + this.commentAndDeclarationEdits = commentAndDeclarationEdits; + } + } + + /** + * Maps by identity each import (as key), except the last, to the import (as value) which comes + * before it. + *

+ * Maps by identity (rather than by hashcode) to handle cases of duplicate import declarations. + */ + private static Map mapPrecedingImports(Collection importEntries) { + Map precedingImports = + new IdentityHashMap(importEntries.size()); + + ImportEntry previousImport = null; + for (ImportEntry currentImport : importEntries) { + ImportName currentImportName = currentImport.importName; + precedingImports.put(currentImportName, previousImport); + previousImport = currentImport; + } + + return precedingImports; + } + + private static boolean containsFloatingComment(Iterable comments) { + for (ImportComment comment : comments) { + if (comment.succeedingLineDelimiters > 1) { + return true; + } + } + + return false; + } + + private final String lineDelimiter; + private final String twoLineDelimiters; + private final boolean fixAllLineDelimiters; + private final int lineDelimitersBetweenImportGroups; + private final ImportGroupComparator importGroupComparator; + private final RemovedImportCommentReassigner commentReassigner; + private final Map originalPrecedingImports; + private final List originalImportEntries; + private final RewriteSite rewriteSite; + private final ImportDeclarationWriter declarationWriter; + + /** + * @param lineDelimiter + * the string to use as a line delimiter when generating text edits + * @param fixAllLineDelimiters + * specifies whether to standardize whitespace between all imports (if true), or only + * between pairs of imports not originally subsequent (if false) + * @param lineDelimitersBetweenImportGroups + * the number of line delimiters desired between import declarations matching + * different import groups + * @param importGroupComparator + * used to determine whether two subsequent imports match the same import group + * @param originalImports + * the original order of imports in the compilation unit + * @param rewriteSite + * describes the location in the compilation unit where imports shall be rewriten + * @param importDeclarationWriter + * used to render each new import declaration (one not originally present in the + * compilation unit) as a string + */ + ImportEditor( + String lineDelimiter, + boolean fixAllLineDelimiters, + int lineDelimitersBetweenImportGroups, + ImportGroupComparator importGroupComparator, + List originalImports, + RewriteSite rewriteSite, + ImportDeclarationWriter importDeclarationWriter) { + this.lineDelimiter = lineDelimiter; + this.twoLineDelimiters = this.lineDelimiter.concat(this.lineDelimiter); + this.fixAllLineDelimiters = fixAllLineDelimiters; + this.lineDelimitersBetweenImportGroups = lineDelimitersBetweenImportGroups; + this.importGroupComparator = importGroupComparator; + this.originalImportEntries = originalImports; + this.rewriteSite = rewriteSite; + this.declarationWriter = importDeclarationWriter; + + this.commentReassigner = new RemovedImportCommentReassigner(originalImports); + + if (fixAllLineDelimiters) { + this.originalPrecedingImports = Collections.emptyMap(); + } else { + this.originalPrecedingImports = Collections.unmodifiableMap(mapPrecedingImports(originalImports)); + } + } + + /** + * Generates and returns a TextEdit to replace or update the import declarations in the + * compilation unit to match the given list. + *

+ * Standardizes whitespace between subsequent imports to the correct number of line delimiters + * (either for every pair of subsequent imports, or only for pairs not originally subsequent, + * depending on the value of {@link #fixAllLineDelimiters}). + *

+ * Relocates leading and trailing comments of removed imports as determined by + * {@link #commentReassigner}. + */ + TextEdit createTextEdit(Collection resultantImports) { + TextEdit edit = new MultiTextEdit(); + + IRegion surroundingRegion = this.rewriteSite.surroundingRegion; + + if (resultantImports.isEmpty()) { + if (this.originalImportEntries.isEmpty()) { + // Leave the compilation unit as is. + } + else { + // Replace original imports and surrounding whitespace with enough line delimiters + // around preceding and/or succeeding elements. + + String newWhitespace; + if (this.rewriteSite.hasPrecedingElements) { + int newDelims = this.rewriteSite.hasSucceedingElements ? 2 : 1; + newWhitespace = createDelimiter(newDelims); + } else { + newWhitespace = ""; //$NON-NLS-1$ + } + + edit.addChild(new ReplaceEdit( + surroundingRegion.getOffset(), surroundingRegion.getLength(), newWhitespace)); + } + } + else { + if (this.originalImportEntries.isEmpty()) { + // Replace existing whitespace with preceding line delimiters, import declarations, + // and succeeding line delimiters. + + Collection importEdits = determineEditsForImports( + surroundingRegion, resultantImports); + + if (this.rewriteSite.hasPrecedingElements) { + edit.addChild(new InsertEdit(surroundingRegion.getOffset(), createDelimiter(2))); + } + + edit.addChildren(importEdits.toArray(new TextEdit[importEdits.size()])); + + int newSucceedingDelims = this.rewriteSite.hasSucceedingElements ? 2 : 1; + String newSucceeding = createDelimiter(newSucceedingDelims); + edit.addChild(new InsertEdit(surroundingRegion.getOffset(), newSucceeding)); + } + else { + // Replace original imports with new ones, leaving surrounding whitespace in place. + + Collection importEdits = determineEditsForImports( + this.rewriteSite.importsRegion, resultantImports); + + edit.addChildren(importEdits.toArray(new TextEdit[importEdits.size()])); + } + } + return edit; + } + + /** + * Concatenates the given number of line delimiters into a single string. + */ + private String createDelimiter(int numberOfLineDelimiters) { + if (numberOfLineDelimiters < 1) { + throw new IllegalArgumentException(); + } + + if (numberOfLineDelimiters == 1) { + return this.lineDelimiter; + } + + if (numberOfLineDelimiters == 2) { + return this.twoLineDelimiters; + } + + StringBuilder correctDelimiter = new StringBuilder(); + for (int i = 0; i < numberOfLineDelimiters; i++) { + correctDelimiter.append(this.lineDelimiter); + } + return correctDelimiter.toString(); + } + + private Collection determineEditsForImports( + IRegion importsRegion, + Collection resultantImports) { + Collection edits = new ArrayList(); + + Map> commentReassignments = + this.commentReassigner.reassignComments(resultantImports); + + OriginalImportsCursor cursor = new OriginalImportsCursor( + importsRegion.getOffset(), this.originalImportEntries); + + edits.addAll(placeResultantImports(cursor, resultantImports, commentReassignments)); + + edits.addAll(deleteRemainingText(importsRegion, edits)); + + // Omit the RangeMarkers used temporarily to mark the text of non-relocated imports. + Collection editsWithoutRangeMarkers = new ArrayList(edits.size()); + for (TextEdit edit : edits) { + if (!(edit instanceof RangeMarker)) { + editsWithoutRangeMarkers.add(edit); + } + } + + return editsWithoutRangeMarkers; + } + + /** + * Creates TextEdits that place each resultant import in the correct (rewritten) position. + */ + private Collection placeResultantImports( + OriginalImportsCursor cursor, + Collection resultantImports, + Map> commentReassignments) { + Collection edits = new ArrayList(); + + ImportEntry lastResultantImport = null; + for (ImportEntry currentResultantImport : resultantImports) { + if (currentResultantImport.isOriginal()) { + // Skip forward to this import's place in the original order. + while (cursor.currentOriginalImport != null + && cursor.currentOriginalImport != currentResultantImport) { + cursor.advance(); + } + } + + Collection reassignedComments = commentReassignments.get(currentResultantImport); + if (reassignedComments == null) { + reassignedComments = Collections.emptyList(); + } + + ImportEdits importPlacement; + if (currentResultantImport.isOriginal()) { + OriginalImportEntry originalImport = currentResultantImport.asOriginalImportEntry(); + if (cursor.currentOriginalImport == currentResultantImport) { + importPlacement = preserveStationaryImport(originalImport); + } else { + importPlacement = moveOriginalImport(originalImport, cursor.currentPosition); + } + } else { + importPlacement = placeNewImport(currentResultantImport, cursor.currentPosition); + } + + String newDelimiter = determineNewDelimiter( + lastResultantImport, currentResultantImport, reassignedComments); + if (newDelimiter == null) { + edits.addAll(importPlacement.leadingDelimiterEdits); + } else if (!newDelimiter.isEmpty()) { + edits.add(new InsertEdit(cursor.currentPosition, newDelimiter)); + } + + if (!reassignedComments.isEmpty()) { + edits.addAll(relocateComments(reassignedComments, cursor.currentPosition)); + + boolean hasFloatingComment = currentResultantImport.isOriginal() + && containsFloatingComment(currentResultantImport.asOriginalImportEntry().comments); + String delimiterAfterReassignedComments = + hasFloatingComment ? this.twoLineDelimiters : this.lineDelimiter; + edits.add(new InsertEdit(cursor.currentPosition, delimiterAfterReassignedComments)); + } + + edits.addAll(importPlacement.commentAndDeclarationEdits); + + if (currentResultantImport == cursor.currentOriginalImport) { + cursor.advance(); + } + + lastResultantImport = currentResultantImport; + } + + return edits; + } + + /** + * Creates text edits to insert the text of a new import. + */ + private ImportEdits placeNewImport(ImportEntry currentResultantImport, int position) { + String declaration = this.declarationWriter.writeImportDeclaration(currentResultantImport.importName); + return new ImportEdits( + Collections.emptySet(), + Collections.singleton(new InsertEdit(position, declaration))); + } + + /** + * Creates text edits to move an import's text to a new position. + */ + private ImportEdits moveOriginalImport(OriginalImportEntry importEntry, int position) { + MoveSourceEdit leadingSourceEdit = new MoveSourceEdit( + importEntry.leadingDelimiter.getOffset(), importEntry.leadingDelimiter.getLength()); + MoveTargetEdit leadingTargetEdit = new MoveTargetEdit(position, leadingSourceEdit); + Collection leadingDelimiterEdits = Arrays.asList(leadingSourceEdit, leadingTargetEdit); + + MoveSourceEdit importSourceEdit = new MoveSourceEdit( + importEntry.declarationAndComments.getOffset(), importEntry.declarationAndComments.getLength()); + MoveTargetEdit importTargetEdit = new MoveTargetEdit(position, importSourceEdit); + Collection declarationAndCommentEdits = Arrays.asList(importSourceEdit, importTargetEdit); + + return new ImportEdits(leadingDelimiterEdits, declarationAndCommentEdits); + } + + /** + * Creates RangeMarkers to mark a non-relocated import's text to prevent its deletion. + */ + private ImportEdits preserveStationaryImport(OriginalImportEntry importEntry) { + return new ImportEdits( + Collections.singleton(new RangeMarker( + importEntry.leadingDelimiter.getOffset(), + importEntry.leadingDelimiter.getLength())), + Collections.singleton(new RangeMarker( + importEntry.declarationAndComments.getOffset(), + importEntry.declarationAndComments.getLength()))); + + } + + /** + * Determines whether and how to standardize the whitespace between the end of the previous + * import (or its last trailing comment) and the start of the current import (or its first + * leading comment). + *

+ * Returns a string containing the correct whitespace to place between the two imports, or + * {@code null} if the current import's original leading whitespace should be preserved. + */ + private String determineNewDelimiter( + ImportEntry lastImport, + ImportEntry currentImport, + Collection reassignedComments) { + if (lastImport == null) { + // The first import in the compilation unit needs no preceding line delimiters. + return ""; //$NON-NLS-1$ + } + + boolean hasReassignedComments = !reassignedComments.isEmpty(); + + if (!needsStandardDelimiter(lastImport, currentImport, hasReassignedComments)) { + return null; + } + + int numberOfLineDelimiters = 1; + + Collection leadingComments; + if (hasReassignedComments) { + leadingComments = reassignedComments; + } else if (currentImport.isOriginal()) { + leadingComments = currentImport.asOriginalImportEntry().comments; + } else { + leadingComments = Collections.emptyList(); + } + if (containsFloatingComment(leadingComments)) { + // Prevent a floating leading comment from becoming attached to the preceding import. + numberOfLineDelimiters = 2; + } + + if (this.importGroupComparator.compare(lastImport.importName, currentImport.importName) != 0) { + // Separate imports belonging to different import groups. + numberOfLineDelimiters = Math.max(numberOfLineDelimiters, this.lineDelimitersBetweenImportGroups); + } + + String standardDelimiter = createDelimiter(numberOfLineDelimiters); + + // Reuse the original preceding delimiter if it matches the standard delimiter, but only + // if there are no reassigned comments (which would necessitate relocating the delimiter). + if (currentImport.isOriginal() && !hasReassignedComments) { + OriginalImportEntry originalImport = currentImport.asOriginalImportEntry(); + IRegion originalDelimiter = originalImport.leadingDelimiter; + if (originalImport.precedingLineDelimiters == numberOfLineDelimiters) { + boolean delimiterIsSameLength = originalDelimiter == null && standardDelimiter.isEmpty() + || originalDelimiter != null && originalDelimiter.getLength() == standardDelimiter.length(); + if (delimiterIsSameLength) { + return null; + } + } + } + + return standardDelimiter; + } + + /** + * Determines whether the whitespace between two subsequent imports should be set to a standard + * number of line delimiters. + */ + private boolean needsStandardDelimiter( + ImportEntry lastImport, + ImportEntry currentImport, + boolean hasReassignedComments) { + boolean needsStandardDelimiter = false; + + if (this.fixAllLineDelimiters) { + // In "Organize Imports" mode, all delimiters between imports are standardized. + needsStandardDelimiter = true; + } else if (!currentImport.isOriginal()) { + // This (new) import does not have an original leading delimiter. + needsStandardDelimiter = true; + } else if (hasReassignedComments) { + // Comments reassigned from removed imports are being prepended to this import. + needsStandardDelimiter = true; + } else { + ImportEntry originalPrecedingImport = this.originalPrecedingImports.get(currentImport.importName); + if (originalPrecedingImport == null || lastImport.importName != originalPrecedingImport.importName) { + // This import follows a different import post-rewrite than pre-rewrite. + needsStandardDelimiter = true; + } + } + + return needsStandardDelimiter; + } + + private Collection relocateComments(Collection reassignedComments, int insertPosition) { + if (reassignedComments.isEmpty()) { + return Collections.emptyList(); + } + + Collection edits = new ArrayList(reassignedComments.size() * 3); + + ImportComment lastComment = null; + for (ImportComment currentComment : reassignedComments) { + MoveSourceEdit sourceEdit = new MoveSourceEdit( + currentComment.region.getOffset(), currentComment.region.getLength()); + edits.add(sourceEdit); + + if (lastComment != null) { + // Preserve blank lines between comments. + int succeedingLineDelimiters = lastComment.succeedingLineDelimiters > 1 ? 2 : 1; + + edits.add(new InsertEdit(insertPosition, createDelimiter(succeedingLineDelimiters))); + } + + edits.add(new MoveTargetEdit(insertPosition, sourceEdit)); + + lastComment = currentComment; + } + + return edits; + } + + /** + * Creates TextEdits that delete text remaining between and after resultant imports. + */ + private static Collection deleteRemainingText(IRegion importRegion, Collection edits) { + List sortedEdits = new ArrayList(edits); + Collections.sort(sortedEdits, new Comparator() { + @Override + public int compare(TextEdit o1, TextEdit o2) { + return o1.getOffset() - o2.getOffset(); + } + }); + + int deletePosition = importRegion.getOffset(); + + Collection deleteRemainingTextEdits = new ArrayList(); + for (TextEdit edit : sortedEdits) { + if (edit.getOffset() > deletePosition) { + deleteRemainingTextEdits.add(new DeleteEdit(deletePosition, edit.getOffset() - deletePosition)); + } + + int editEndPosition = edit.getOffset() + edit.getLength(); + deletePosition = Math.max(deletePosition, editEndPosition); + } + + // Delete text remaining after the last import. + int importRegionEndPosition = importRegion.getOffset() + importRegion.getLength(); + if (deletePosition < importRegionEndPosition) { + deleteRemainingTextEdits.add(new DeleteEdit(deletePosition, importRegionEndPosition - deletePosition)); + } + + return deleteRemainingTextEdits; + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportEntry.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportEntry.java new file mode 100644 index 0000000..d93b86a --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportEntry.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +/** + * Describes an import declaration, encapsulating the imported name and, for an import declaration + * originally present in the compilation unit, the regions originally occupied by the import + * declaration and its associated comments. + *

+ * As the Java Language Specification allows duplicate import declarations, a compilation unit + * may contain multiple {@code ImportEntry}s with equal {@code ImportName}s. + */ +abstract class ImportEntry { + final ImportName importName; + + protected ImportEntry(ImportName importName) { + this.importName = importName; + } + + /** + * Returns true if this import declaration occurred originally (before the rewrite). + */ + abstract boolean isOriginal(); + + /** + * If this import declaration occurred originally, returns it as an OriginalImportEntry; + * otherwise throws an exception. + */ + abstract OriginalImportEntry asOriginalImportEntry(); +} diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportGroupComparator.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportGroupComparator.java new file mode 100644 index 0000000..178dcc1 --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportGroupComparator.java @@ -0,0 +1,197 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; + +/** + * Sorts imports according to the order of import groups defined on the Organize Imports preference + * page. Considers equal any two imports matching the same import group. + */ +final class ImportGroupComparator implements Comparator{ + private static final class ImportGroup { + private final String name; + private final int index; + private final ImportGroup prefix; + + public ImportGroup(String name, int index, ImportGroup prefix) { + this.name = name; + this.index = index; + this.prefix = prefix; + } + + @Override + public String toString() { + return String.format("ImportGroup(%d:%s)", getIndex(), getName()); //$NON-NLS-1$ + } + + String getName() { + return this.name; + } + + int getIndex() { + return this.index; + } + + ImportGroup getPrefix() { + return this.prefix; + } + } + + private static final class IndexedImportGroups { + final NavigableMap typeImportGroupsByName; + final NavigableMap staticImportGroupByName; + + IndexedImportGroups( + NavigableMap typeImportGroupsByName, + NavigableMap staticImportGroupsByName) { + this.typeImportGroupsByName = typeImportGroupsByName; + this.staticImportGroupByName = staticImportGroupsByName; + } + } + + private static final String MATCH_ALL = ""; //$NON-NLS-1$ + private static final String STATIC_PREFIX = "#"; //$NON-NLS-1$ + private static final String STATIC_MATCH_ALL = STATIC_PREFIX + MATCH_ALL; + + private static List memoizedImportOrder = null; + private static IndexedImportGroups memoizedIndexedImportGroups = null; + + private static List includeMatchAllImportGroups(List importOrder) { + boolean needsTypeMatchAll = !importOrder.contains(MATCH_ALL); + boolean needsStaticMatchAll = !importOrder.contains(STATIC_MATCH_ALL); + + if (!needsTypeMatchAll && !needsStaticMatchAll) { + return importOrder; + } + + List augmentedOrder = new ArrayList(importOrder.size() + 2); + + if (needsStaticMatchAll) { + augmentedOrder.add(STATIC_MATCH_ALL); + } + + augmentedOrder.addAll(importOrder); + + if (needsTypeMatchAll) { + augmentedOrder.add(MATCH_ALL); + } + + return augmentedOrder; + } + + private static synchronized IndexedImportGroups indexImportOrder(List importOrder) { + if (importOrder.equals(memoizedImportOrder)) { + return memoizedIndexedImportGroups; + } + + Map typeGroupsAndIndices = new HashMap(); + Map staticGroupsAndIndices = new HashMap(); + for (int i = 0; i < importOrder.size(); i++) { + String importGroupString = importOrder.get(i); + + final Map groupsAndIndices; + if (importGroupString.startsWith(STATIC_PREFIX)) { + groupsAndIndices = staticGroupsAndIndices; + importGroupString = importGroupString.substring(1); + } else { + groupsAndIndices = typeGroupsAndIndices; + } + + groupsAndIndices.put(importGroupString, i); + } + + memoizedImportOrder = importOrder; + + memoizedIndexedImportGroups = new IndexedImportGroups( + mapImportGroups(typeGroupsAndIndices), + mapImportGroups(staticGroupsAndIndices)); + + return memoizedIndexedImportGroups; + } + + private static NavigableMap mapImportGroups(Map importGroupNamesAndIndices) { + if (importGroupNamesAndIndices.isEmpty()) { + importGroupNamesAndIndices = Collections.singletonMap(MATCH_ALL, 0); + } + + List sortedNames = new ArrayList(importGroupNamesAndIndices.keySet()); + Collections.sort(sortedNames); + + ArrayList importGroups = new ArrayList(sortedNames.size()); + + Deque prefixingGroups = new ArrayDeque(); + for (String name : sortedNames) { + while (!prefixingGroups.isEmpty() + && !isWholeSegmentPrefix(prefixingGroups.getLast().getName(), name)) { + prefixingGroups.removeLast(); + } + ImportGroup prefix = prefixingGroups.peekLast(); + + ImportGroup group = new ImportGroup(name, importGroupNamesAndIndices.get(name), prefix); + + importGroups.add(group); + + prefixingGroups.addLast(group); + } + + NavigableMap groupsByName = new TreeMap(); + for (ImportGroup group : importGroups) { + groupsByName.put(group.getName(), group); + } + + return groupsByName; + } + + private static boolean isWholeSegmentPrefix(String prefix, String name) { + if (!name.startsWith(prefix)) { + return false; + } + + return prefix.isEmpty() || name.length() == prefix.length() || name.charAt(prefix.length()) == '.'; + } + + private final IndexedImportGroups indexedImportGroups; + + ImportGroupComparator(List importOrder) { + List importOrderWithMatchAllGroups = includeMatchAllImportGroups(importOrder); + this.indexedImportGroups = indexImportOrder(importOrderWithMatchAllGroups); + } + + @Override + public int compare(ImportName o1, ImportName o2) { + return determineSortPosition(o1) - determineSortPosition(o2); + } + + private int determineSortPosition(ImportName importName) { + String name = (importName.isOnDemand() ? importName.containerName : importName.qualifiedName); + + NavigableMap groupsByName = importName.isStatic + ? this.indexedImportGroups.staticImportGroupByName + : this.indexedImportGroups.typeImportGroupsByName; + + ImportGroup prefixingGroup = groupsByName.floorEntry(name).getValue(); + while (!isWholeSegmentPrefix(prefixingGroup.getName(), name)) { + prefixingGroup = prefixingGroup.getPrefix(); + } + + return prefixingGroup.getIndex(); + } +} diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportName.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportName.java new file mode 100644 index 0000000..2f432f0 --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportName.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import org.eclipse.jdt.core.Signature; +import org.eclipse.jdt.core.dom.ImportDeclaration; + +/** + * Encapsulates an import's fully qualified name, whether it is on-demand, and whether it is static. + *

+ * The fully qualified name is divided into two parts: + *

    + *
  • a container name, which is everything preceding the last dot ('.'). + *
  • a simple name, which is the part following the last dot ("*" for an on-demand import). + *
+ */ +public final class ImportName { + static ImportName createFor(ImportDeclaration importDeclaration) { + String declName = importDeclaration.getName().getFullyQualifiedName(); + if (importDeclaration.isOnDemand()) { + return createOnDemand(importDeclaration.isStatic(), declName); + } + return createFor(importDeclaration.isStatic(), declName); + } + + static ImportName createOnDemand(boolean isStatic, String containerName) { + return new ImportName(isStatic, containerName, "*"); //$NON-NLS-1$ + } + + public static ImportName createFor(boolean isStatic, String qualifiedName) { + String containerName = Signature.getQualifier(qualifiedName); + String simpleName = qualifiedName.substring(containerName.length() + 1); + return new ImportName(isStatic, containerName, simpleName); + } + + public final boolean isStatic; + public final String containerName; + public final String simpleName; + public final String qualifiedName; + + private ImportName(boolean isStatic, String containerName, String simpleName) { + this.isStatic = isStatic; + this.containerName = containerName; + this.simpleName = simpleName; + + this.qualifiedName = this.containerName + "." + this.simpleName; //$NON-NLS-1$; + } + + @Override + public String toString() { + String template = this.isStatic ? "staticImport(%s)" : "typeImport(%s)"; //$NON-NLS-1$ //$NON-NLS-2$ + return String.format(template, this.qualifiedName); + } + + @Override + public int hashCode() { + int result = this.qualifiedName.hashCode(); + result = 31 * result + (this.isStatic ? 1 : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ImportName)) { + return false; + } + + ImportName other = (ImportName) obj; + + return this.qualifiedName.equals(other.qualifiedName) && this.isStatic == other.isStatic; + } + + public boolean isOnDemand() { + return this.simpleName.equals("*"); //$NON-NLS-1$ + } + + /** + * Returns an on-demand ImportName with the same isStatic and containerName as this ImportName. + */ + ImportName getContainerOnDemand() { + if (this.isOnDemand()) { + return this; + } + + return ImportName.createOnDemand(this.isStatic, this.containerName); + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportRewriteAnalyzer.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportRewriteAnalyzer.java new file mode 100644 index 0000000..7497b09 --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportRewriteAnalyzer.java @@ -0,0 +1,650 @@ +/******************************************************************************* + * Copyright (c) 2000, 2013, 2014, 2015 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + * Stephan Herrmann - Contribution for Bug 378024 - Ordering of comments between imports not preserved + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NavigableMap; +import java.util.Set; +import java.util.TreeMap; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Comment; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ImportDeclaration; +import org.eclipse.jdt.core.dom.PackageDeclaration; +import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants; +import org.eclipse.jdt.core.search.SearchEngine; +import org.eclipse.jdt.internal.core.JavaProject; +import org.eclipse.jdt.internal.core.dom.rewrite.imports.ConflictIdentifier.Conflicts; +import org.eclipse.jdt.internal.core.util.Util; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Region; +import org.eclipse.text.edits.TextEdit; + +/** + * Allows the caller to specify imports to be added to or removed from a compilation unit and + * creates a TextEdit which, applied to the compilation unit, effects the specified additions and + * removals. + *

+ * Operates in either of two modes (selected via {@link ImportRewriteConfiguration.Builder}'s + * static factory methods): + *

    + *
  • Discarding original imports and totally sorting all imports added thereafter. This mode is + * used by the Organize Imports operation.
  • + *
  • Preserving original imports and placing each added import adjacent to the one most closely + * matching it. This mode is used e.g. when Content Assist adds an import for a completed name.
  • + *
+ */ +public final class ImportRewriteAnalyzer { + /** + * Encapsulates, for a computed import rewrite, a {@code TextEdit} that can be applied to effect + * the rewrite as well as the names of imports created by the rewrite. + */ + public static final class RewriteResult { + private final TextEdit textEdit; + private final Set createdImports; + + RewriteResult(TextEdit textEdit, Set createdImports) { + this.textEdit = textEdit; + this.createdImports = Collections.unmodifiableSet(createdImports); + } + + /** + * Returns a {@link TextEdit} describing the changes necessary to perform the rewrite. + */ + public TextEdit getTextEdit() { + return this.textEdit; + } + + public String[] getCreatedImports() { + return extractQualifiedNames(false, this.createdImports); + } + + public String[] getCreatedStaticImports() { + return extractQualifiedNames(true, this.createdImports); + } + + private String[] extractQualifiedNames(boolean b, Collection imports) { + List names = new ArrayList(imports.size()); + for (ImportName importName : imports) { + if (importName.isStatic == b) { + names.add(importName.qualifiedName); + } + } + + return names.toArray(new String[names.size()]); + } + } + + /** + * Returns the value of the formatter option specifying how many blank lines to insert between + * import groups. + */ + private static int getBlankLinesBetweenImportGroups(IJavaProject javaProject) { + int num = -1; + + String blankLinesOptionValue = + javaProject.getOption(DefaultCodeFormatterConstants.FORMATTER_BLANK_LINES_BETWEEN_IMPORT_GROUPS, true); + try { + num = Integer.parseInt(blankLinesOptionValue); + } catch (NumberFormatException e) { + String message = String.format( + "Could not parse the value of %s as an integer: %s", //$NON-NLS-1$ + DefaultCodeFormatterConstants.FORMATTER_BLANK_LINES_BETWEEN_IMPORT_GROUPS, + blankLinesOptionValue); + Util.log(new Status(IStatus.WARNING, JavaCore.PLUGIN_ID, message, e)); + } + + return num >= 0 ? num : 1; + } + + /** + * Returns the value of the formatter option specifying whether to insert a space between the + * imported name and the semicolon in an import declaration. + */ + private static boolean shouldInsertSpaceBeforeSemicolon(IJavaProject javaProject) { + return JavaCore.INSERT.equals( + javaProject.getOption(DefaultCodeFormatterConstants.FORMATTER_INSERT_SPACE_BEFORE_SEMICOLON, true)); + } + + /** + * Reads the positions of each existing import declaration along with any associated comments, + * and returns these in a list whose iteration order reflects the existing order of the imports + * in the compilation unit. + */ + private static List readOriginalImports(CompilationUnit compilationUnit) { + List importDeclarations = compilationUnit.imports(); + + if (importDeclarations.isEmpty()) { + return Collections.emptyList(); + } + + List comments = compilationUnit.getCommentList(); + + int currentCommentIndex = 0; + + // Skip over package and file header comments (see https://bugs.eclipse.org/121428). + ImportDeclaration firstImport = importDeclarations.get(0); + PackageDeclaration packageDeclaration = compilationUnit.getPackage(); + int firstImportStartPosition = packageDeclaration == null + ? firstImport.getStartPosition() + : compilationUnit.getExtendedStartPosition(packageDeclaration) + + compilationUnit.getExtendedLength(packageDeclaration); + while (currentCommentIndex < comments.size() + && comments.get(currentCommentIndex).getStartPosition() < firstImportStartPosition) { + currentCommentIndex++; + } + + List imports = new ArrayList(importDeclarations.size()); + int previousExtendedEndPosition = -1; + for (ImportDeclaration currentImport : importDeclarations) { + int extendedEndPosition = compilationUnit.getExtendedStartPosition(currentImport) + + compilationUnit.getExtendedLength(currentImport); + + int commentAfterImportIndex = currentCommentIndex; + while (commentAfterImportIndex < comments.size() + && comments.get(commentAfterImportIndex).getStartPosition() < extendedEndPosition) { + commentAfterImportIndex++; + } + + List importComments; + if (commentAfterImportIndex == currentCommentIndex) { + importComments = Collections.emptyList(); + } else { + importComments = selectImportComments( + compilationUnit, + comments, + currentImport.getStartPosition(), + currentCommentIndex, + commentAfterImportIndex); + } + + int importAndCommentsStartPosition = importComments.isEmpty() + ? currentImport.getStartPosition() + : Math.min(currentImport.getStartPosition(), importComments.get(0).region.getOffset()); + + IRegion leadingWhitespaceRegion; + int precedingLineDelimiters; + if (previousExtendedEndPosition == -1) { + leadingWhitespaceRegion = new Region(importAndCommentsStartPosition, 0); + precedingLineDelimiters = 0; + } else { + leadingWhitespaceRegion = new Region( + previousExtendedEndPosition, importAndCommentsStartPosition - previousExtendedEndPosition); + int importAndCommentsFirstLine = compilationUnit.getLineNumber(importAndCommentsStartPosition); + int lastLineOfPrevious = compilationUnit.getLineNumber(previousExtendedEndPosition - 1); + precedingLineDelimiters = importAndCommentsFirstLine - lastLineOfPrevious; + } + IRegion importAndCommentsRegion = + new Region(importAndCommentsStartPosition, extendedEndPosition - importAndCommentsStartPosition); + + imports.add(new OriginalImportEntry( + ImportName.createFor(currentImport), + importComments, + precedingLineDelimiters, + leadingWhitespaceRegion, + importAndCommentsRegion)); + + currentCommentIndex = commentAfterImportIndex; + previousExtendedEndPosition = extendedEndPosition; + } + + return imports; + } + + private static List selectImportComments( + CompilationUnit compilationUnit, + List comments, + int importDeclarationStartPosition, + int commentStartIndex, + int commentEndIndex) { + List importComments = new ArrayList(comments.size()); + + Iterator commentIterator = comments.subList(commentStartIndex, commentEndIndex).iterator(); + Comment currentComment = commentIterator.hasNext() ? commentIterator.next() : null; + while (currentComment != null) { + int currentCommentStartPosition = currentComment.getStartPosition(); + int currentCommentLength = currentComment.getLength(); + + Comment nextComment = commentIterator.hasNext() ? commentIterator.next() : null; + + int succeedingLineDelims; + int nextCommentStartPosition = nextComment == null ? Integer.MAX_VALUE : nextComment.getStartPosition(); + int nextStartPosition = Math.min(importDeclarationStartPosition, nextCommentStartPosition); + if (nextStartPosition == Integer.MAX_VALUE) { + // This trailing comment is located at the end of the import's extended range + // and we don't care how many line delimiters follow it. + succeedingLineDelims = 0; + } else { + int currentCommentEndLine = + compilationUnit.getLineNumber(currentCommentStartPosition + currentCommentLength); + int nextStartLine = compilationUnit.getLineNumber(nextStartPosition); + succeedingLineDelims = nextStartLine - currentCommentEndLine; + } + + importComments.add(new ImportComment( + new Region(currentCommentStartPosition, currentCommentLength), succeedingLineDelims)); + + currentComment = nextComment; + } + + return importComments; + } + + private static RewriteSite determineRewriteSite( + CompilationUnit compilationUnit, List originalImports) { + IRegion importsRegion = determineImportsRegion(originalImports); + + IRegion surroundingRegion = determineSurroundingRegion(compilationUnit, importsRegion); + + boolean hasPrecedingElements = surroundingRegion.getOffset() != 0; + + boolean hasSucceedingElements = + surroundingRegion.getOffset() + surroundingRegion.getLength() != compilationUnit.getLength(); + + return new RewriteSite( + surroundingRegion, + importsRegion, + hasPrecedingElements, + hasSucceedingElements); + } + + /** + * Determines the region originally occupied by imports and their associated comments. + *

+ * Returns null if originalImports is null or empty. + */ + private static IRegion determineImportsRegion(List originalImports) { + if (originalImports == null || originalImports.isEmpty()) { + return null; + } + + OriginalImportEntry firstImport = originalImports.get(0); + int start = firstImport.declarationAndComments.getOffset(); + + OriginalImportEntry lastImport = originalImports.get(originalImports.size() - 1); + int end = lastImport.declarationAndComments.getOffset() + + lastImport.declarationAndComments.getLength(); + + return new Region(start, end - start); + } + + /** + * Determines the region to be occupied by imports, their associated comments, and surrounding + * whitespace. + */ + private static IRegion determineSurroundingRegion(CompilationUnit compilationUnit, IRegion importsRegion) { + NavigableMap nodesTreeMap = mapTopLevelNodes(compilationUnit); + + int surroundingStart; + int positionAfterImports; + if (importsRegion == null) { + PackageDeclaration packageDeclaration = compilationUnit.getPackage(); + if (packageDeclaration != null) { + surroundingStart = compilationUnit.getExtendedStartPosition(packageDeclaration) + + compilationUnit.getExtendedLength(packageDeclaration); + } + else { + surroundingStart = 0; + } + + positionAfterImports = surroundingStart; + } else { + Entry lowerEntry = nodesTreeMap.lowerEntry(importsRegion.getOffset()); + if (lowerEntry != null) { + ASTNode precedingNode = lowerEntry.getValue(); + surroundingStart = precedingNode.getStartPosition() + precedingNode.getLength(); + } else { + surroundingStart = 0; + } + + positionAfterImports = importsRegion.getOffset() + importsRegion.getLength(); + } + + Integer ceilingKey = nodesTreeMap.ceilingKey(positionAfterImports); + int surroundingEnd = ceilingKey != null ? ceilingKey : compilationUnit.getLength(); + + return new Region(surroundingStart, surroundingEnd - surroundingStart); + } + + /** + * Builds a NavigableMap containing all of the given compilation unit's top-level nodes + * (package declaration, import declarations, type declarations, and non-doc comments), + * keyed by start position. + */ + private static NavigableMap mapTopLevelNodes(CompilationUnit compilationUnit) { + NavigableMap map = new TreeMap(); + + Collection nodes = new ArrayList(); + if (compilationUnit.getPackage() != null) { + nodes.add(compilationUnit.getPackage()); + } + nodes.addAll(compilationUnit.imports()); + nodes.addAll(compilationUnit.types()); + for (Comment comment : ((List) compilationUnit.getCommentList())) { + // Include only top-level (non-doc) comments; + // doc comments are contained within their parent nodes' ranges. + if (comment.getParent() == null) { + nodes.add(comment); + } + } + + for (ASTNode node : nodes) { + map.put(node.getStartPosition(), node); + } + + return map; + } + + /** + * Builds an {@code IdentityHashMap} having the elements of {@code imports} as values and each + * element's {@code importName} as corresponding key. This map can be used to recall the {@code + * ImportEntry} corresponding to a given {@code ImportName} instance even when there are + * duplicate import declarations (where multiple {@code ImportEntry}s have equal, but not + * identical, {@code ImportName}s). + */ + private static Map mapImportsByNameIdentity(List imports) { + Map importsByName = new IdentityHashMap(); + + for (OriginalImportEntry currentImport : imports) { + importsByName.put(currentImport.importName, currentImport); + } + + return Collections.unmodifiableMap(importsByName); + } + + /** + * Returns a new {@code List} containing those elements of {@code imports} (in their existing + * order) not contained in {@code importsToSubtract}. + */ + private static List subtractImports( + Collection existingImports, Set importsToSubtract) { + List remainingImports = new ArrayList(existingImports.size()); + for (ImportName existingImport : existingImports) { + if (!importsToSubtract.contains(existingImport)) { + remainingImports.add(existingImport); + } + } + return remainingImports; + } + + private final List originalImportEntries; + private final List originalImportsList; + private final Set originalImportsSet; + + private final ImportDeclarationWriter importDeclarationWriter; + + private final ImportAdder importAdder; + + private final Set importsToAdd; + private final Set importsToRemove; + + private final boolean reportAllResultantImportsAsCreated; + + private final Set typeExplicitSimpleNames; + private final Set staticExplicitSimpleNames; + + private final Set implicitImportContainerNames; + + private final ConflictIdentifier conflictIdentifier; + + private final OnDemandComputer onDemandComputer; + + private final Map importsByNameIdentity; + + private final String lineDelimiter; + + private final ImportEditor importEditor; + + public ImportRewriteAnalyzer( + ICompilationUnit cu, + CompilationUnit astRoot, + ImportRewriteConfiguration configuration) throws JavaModelException { + this.originalImportEntries = Collections.unmodifiableList(readOriginalImports(astRoot)); + + List importsList = new ArrayList(this.originalImportEntries.size()); + Set importsSet = new HashSet(); + for (ImportEntry originalImportEntry : this.originalImportEntries) { + ImportName importName = originalImportEntry.importName; + importsList.add(importName); + importsSet.add(importName); + } + this.originalImportsList = Collections.unmodifiableList(importsList); + this.originalImportsSet = Collections.unmodifiableSet(importsSet); + + this.importsToAdd = new LinkedHashSet(); + + this.importsToRemove = new LinkedHashSet(); + if (configuration.originalImportHandling.shouldRemoveOriginalImports()) { + this.importsToRemove.addAll(importsSet); + this.reportAllResultantImportsAsCreated = true; + } else { + this.reportAllResultantImportsAsCreated = false; + } + + this.typeExplicitSimpleNames = new HashSet(); + this.staticExplicitSimpleNames = new HashSet(); + + ImportGroupComparator importGroupComparator = new ImportGroupComparator(configuration.importOrder); + + JavaProject javaProject = (JavaProject) cu.getJavaProject(); + + this.importAdder = configuration.originalImportHandling.createImportAdder(new ImportComparator( + importGroupComparator, + configuration.typeContainerSorting.createContainerComparator(javaProject), + configuration.staticContainerSorting.createContainerComparator(javaProject))); + + this.implicitImportContainerNames = + configuration.implicitImportIdentification.determineImplicitImportContainers(cu); + + this.onDemandComputer = new OnDemandComputer( + configuration.typeOnDemandThreshold, + configuration.staticOnDemandThreshold); + + this.conflictIdentifier = new ConflictIdentifier( + this.onDemandComputer, + new TypeConflictingSimpleNameFinder(javaProject, new SearchEngine()), + new StaticConflictingSimpleNameFinder(javaProject), + this.implicitImportContainerNames); + + this.importsByNameIdentity = mapImportsByNameIdentity(this.originalImportEntries); + + this.importDeclarationWriter = new ImportDeclarationWriter(shouldInsertSpaceBeforeSemicolon(javaProject)); + + this.lineDelimiter = cu.findRecommendedLineSeparator(); + + this.importEditor = new ImportEditor( + this.lineDelimiter, + configuration.originalImportHandling.shouldFixAllLineDelimiters(), + getBlankLinesBetweenImportGroups(javaProject) + 1, + importGroupComparator, + this.originalImportEntries, + determineRewriteSite(astRoot, this.originalImportEntries), + this.importDeclarationWriter); + } + + /** + * Specifies that applying the rewrite should result in the compilation unit containing the + * specified import. + *

+ * Has no effect if the compilation unit otherwise would contain the given import. + *

+ * Overrides any previous corresponding call to {@link #removeImport}. + */ + public void addImport(boolean isStatic, String qualifiedName) { + ImportName importToAdd = ImportName.createFor(isStatic, qualifiedName); + this.importsToAdd.add(importToAdd); + this.importsToRemove.remove(importToAdd); + } + + /** + * Specifies that applying the rewrite should result in the compilation unit not containing the + * specified import. + *

+ * Has no effect if the compilation unit otherwise would not contain the given import. + *

+ * Overrides any previous corresponding call to {@link #addImport}. + */ + public void removeImport(boolean isStatic, String qualifiedName) { + ImportName importToRemove = ImportName.createFor(isStatic, qualifiedName); + this.importsToAdd.remove(importToRemove); + this.importsToRemove.add(importToRemove); + } + + /** + * Specifies that any import of the given simple name must be explicit - that it may neither be + * reduced into an on-demand (".*") import nor be filtered as an implicit (e.g. "java.lang.*") + * import. + */ + public void requireExplicitImport(boolean isStatic, String simpleName) { + if (isStatic) { + this.staticExplicitSimpleNames.add(simpleName); + } else { + this.typeExplicitSimpleNames.add(simpleName); + } + } + + /** + * Computes and returns the result of performing the rewrite, incorporating all changes + * specified by calls to {@link #addImport}, {@link #removeImport}, and + * {@link #requireExplicitImport}. + *

+ * This method has no side-effects. + */ + public RewriteResult analyzeRewrite(IProgressMonitor monitor) throws JavaModelException { + List computedImportOrder = computeImportOrder(monitor); + + List resultingImportEntries = matchExistingOrCreateNew(computedImportOrder); + + TextEdit edit = this.importEditor.createTextEdit(resultingImportEntries); + + Set createdImports = new HashSet(computedImportOrder); + if (!this.reportAllResultantImportsAsCreated) { + createdImports.removeAll(this.originalImportsSet); + } + + return new RewriteResult(edit, createdImports); + } + + private List computeImportOrder(IProgressMonitor progressMonitor) throws JavaModelException { + Set addedImports = Collections.unmodifiableSet(this.importsToAdd); + + Set importsWithAdditionsAndRemovals = new HashSet(this.originalImportsSet); + importsWithAdditionsAndRemovals.addAll(addedImports); + importsWithAdditionsAndRemovals.removeAll(this.importsToRemove); + + Conflicts conflicts = this.conflictIdentifier.identifyConflicts( + importsWithAdditionsAndRemovals, + addedImports, + this.typeExplicitSimpleNames, + this.staticExplicitSimpleNames, + progressMonitor); + + Set allTypeExplicitSimpleNames = new HashSet(this.typeExplicitSimpleNames); + allTypeExplicitSimpleNames.addAll(conflicts.typeConflicts); + + Set allStaticExplicitSimpleNames = new HashSet(this.staticExplicitSimpleNames); + allStaticExplicitSimpleNames.addAll(conflicts.staticConflicts); + + Set implicitImports = identifyImplicitImports(addedImports, allTypeExplicitSimpleNames); + List importsWithoutImplicits = + subtractImports(importsWithAdditionsAndRemovals, implicitImports); + + Collection onDemandReductions = this.onDemandComputer.identifyPossibleReductions( + new HashSet(importsWithoutImplicits), + addedImports, + allTypeExplicitSimpleNames, + allStaticExplicitSimpleNames); + + ImportsDelta delta = computeDelta(implicitImports, onDemandReductions); + + List importsWithRemovals = subtractImports(this.originalImportsList, delta.importsToRemove); + + List importsWithAdditions = this.importAdder.addImports(importsWithRemovals, delta.importsToAdd); + + return importsWithAdditions; + } + + private Set identifyImplicitImports( + Collection addedImports, Set allTypeExplicitSimpleNames) { + if (this.implicitImportContainerNames.isEmpty()) { + return Collections.emptySet(); + } + + Collection implicits = new ArrayList(addedImports.size()); + for (ImportName addedImport : addedImports) { + boolean isImplicit = this.implicitImportContainerNames.contains(addedImport.containerName) + && !allTypeExplicitSimpleNames.contains(addedImport.simpleName); + if (isImplicit) { + implicits.add(addedImport); + } + } + + if (implicits.isEmpty()) { + return Collections.emptySet(); + } + + return new HashSet(implicits); + } + + private List matchExistingOrCreateNew(Collection importNames) { + List importEntries = new ArrayList(importNames.size()); + for (ImportName importName : importNames) { + ImportEntry importEntry = this.importsByNameIdentity.get(importName); + + if (importEntry == null) { + importEntry = new NewImportEntry(importName); + } + + importEntries.add(importEntry); + } + return importEntries; + } + + private ImportsDelta computeDelta( + Collection implicitImports, Collection onDemandReductions) { + Collection additions = new ArrayList(this.originalImportsList.size()); + additions.addAll(this.importsToAdd); + + Collection removals = new ArrayList(this.originalImportsList.size()); + removals.addAll(this.importsToRemove); + removals.addAll(implicitImports); + + additions.removeAll(removals); + + for (OnDemandReduction onDemandReduction : onDemandReductions) { + additions.removeAll(onDemandReduction.reducibleImports); + removals.addAll(onDemandReduction.reducibleImports); + + additions.add(onDemandReduction.containerOnDemand); + removals.remove(onDemandReduction.containerOnDemand); + } + + return new ImportsDelta(additions, removals); + } +} diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportRewriteConfiguration.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportRewriteConfiguration.java new file mode 100644 index 0000000..c77fee8 --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportRewriteConfiguration.java @@ -0,0 +1,261 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.internal.core.JavaProject; + +/** + * Used as a constructor parameter to ImportRewriteAnalyzer to configure its behavior. + *

+ * The starting points are the two static factory methods of {@link Builder}. + */ +public final class ImportRewriteConfiguration { + public enum OriginalImportHandling { + /** + * Specifies to discard original imports and totally sort all new imports, as in the case of + * the "Organize Imports" operation. + */ + DISCARD { + @Override + boolean shouldRemoveOriginalImports() { + return true; + } + + @Override + boolean shouldFixAllLineDelimiters() { + return true; + } + + @Override + ImportAdder createImportAdder(Comparator importComparator) { + return new ReorderingImportAdder(importComparator); + } + }, + /** + * Specifies to keep original imports in their original order, placing each newly added + * import adjacent to the original import that it most closely matches. + */ + PRESERVE_IN_ORDER { + @Override + boolean shouldRemoveOriginalImports() { + return false; + } + + @Override + boolean shouldFixAllLineDelimiters() { + return false; + } + + @Override + ImportAdder createImportAdder(Comparator importComparator) { + return new OrderPreservingImportAdder(importComparator); + } + }, + ; + + /** + * If true, ImportRewriteAnalyzer will, during its initialization, mark all original imports + * for removal. + */ + abstract boolean shouldRemoveOriginalImports(); + + /** + * If true, line delimiters will be standardized between every pair of adjacent imports. + * Otherwise, line delimiters will be corrected only between pairs of adjacent imports that + * were not adjacent originally. + */ + abstract boolean shouldFixAllLineDelimiters(); + + /** + * Creates the {@link ImportAdder} which will combine and order new and existing imports + * together. + */ + abstract ImportAdder createImportAdder(Comparator importComparator); + } + + /** + * Specifies how to sort import declarations by their packages and/or containing types. + */ + public enum ImportContainerSorting { + /** + * Sorts imports by each import's package and any containing types, in lexicographic order. + * For example (assuming that all of the imports belong to the same import group): + *

+		 * import java.net.Socket;
+		 * import java.util.Map;
+		 * import java.util.Set;
+		 * import java.util.Map.Entry;
+		 * 
+ */ + BY_PACKAGE_AND_CONTAINING_TYPE { + @Override + Comparator createContainerComparator(JavaProject javaProject) { + return new PackageAndContainingTypeImportComparator(); + } + }, + + /** + * Sorts imports by each import's package, in lexicographic order. For example (assuming all + * of the imports belong to the same import group): + *
+		 * import java.net.Socket;
+		 * import java.util.Map;
+		 * import java.util.Map.Entry;
+		 * import java.util.Set;
+		 * 
+ */ + BY_PACKAGE { + @Override + Comparator createContainerComparator(JavaProject javaProject) { + return new PackageImportComparator(javaProject); + } + }, + ; + + abstract Comparator createContainerComparator(JavaProject javaProject); + } + + /** + * Specifies which types are considered to be implicitly imported. + *

+ * An import declaration of such a type will not be added to the compilation unit unless it is + * needed to resolve a conflict with an on-demand imports, or the type's simple name has been + * specified with {@link ImportRewriteAnalyzer#requireExplicitImport}. + *

+ * Also, implicitly imported types will be considered for conflicts when deciding which types + * from other packages can be reduced into on-demand imports. E.g. if java.lang.Integer were + * considered to be implicitly imported, that would prevent an import of com.example.Integer + * from being reduced into an on-demand import of com.example.*. + */ + public enum ImplicitImportIdentification { + /** + * Specifies that types from the following packages are considered to be implicitly + * imported: + *

    + *
  • java.lang
  • + *
  • the package of the compilation unit being rewritten
  • + *
+ */ + JAVA_LANG_AND_CU_PACKAGE { + @Override + Set determineImplicitImportContainers(ICompilationUnit compilationUnit) { + Set implicitImportContainerNames = new HashSet(); + + implicitImportContainerNames.add("java.lang"); //$NON-NLS-1$ + + String compilationUnitPackageName = compilationUnit.getParent().getElementName(); + implicitImportContainerNames.add(compilationUnitPackageName); + + return implicitImportContainerNames; + } + }, + /** + * Specifies that no types are considered to be implicitly imported. + */ + NONE { + @Override + Set determineImplicitImportContainers(ICompilationUnit compilationUnit) { + return Collections.emptySet(); + } + }, + ; + + abstract Set determineImplicitImportContainers(ICompilationUnit compilationUnit); + } + + public static class Builder { + public static Builder discardingOriginalImports() { + return new Builder(OriginalImportHandling.DISCARD); + } + + public static Builder preservingOriginalImports() { + return new Builder(OriginalImportHandling.PRESERVE_IN_ORDER); + } + + final OriginalImportHandling originalImportHandling; + ImportContainerSorting typeContainerSorting; + ImportContainerSorting staticContainerSorting; + ImplicitImportIdentification implicitImportIdentification; + List importOrder; + Integer typeOnDemandThreshold; + Integer staticOnDemandThreshold; + + private Builder(OriginalImportHandling originalImportHandling) { + this.originalImportHandling = originalImportHandling; + this.typeContainerSorting = ImportContainerSorting.BY_PACKAGE; + this.staticContainerSorting = ImportContainerSorting.BY_PACKAGE_AND_CONTAINING_TYPE; + this.implicitImportIdentification = ImplicitImportIdentification.JAVA_LANG_AND_CU_PACKAGE; + this.importOrder = Collections.emptyList(); + this.typeOnDemandThreshold = null; + this.staticOnDemandThreshold = null; + } + + public Builder setTypeContainerSorting(ImportContainerSorting typeContainerSorting) { + this.typeContainerSorting = typeContainerSorting; + return this; + } + + public Builder setStaticContainerSorting(ImportContainerSorting staticContainerSorting) { + this.staticContainerSorting = staticContainerSorting; + return this; + } + + public Builder setImplicitImportIdentification(ImplicitImportIdentification implicitImportIdentification) { + this.implicitImportIdentification = implicitImportIdentification; + return this; + } + + public Builder setImportOrder(List importOrder) { + this.importOrder = Collections.unmodifiableList(new ArrayList(importOrder)); + return this; + } + + public Builder setTypeOnDemandThreshold(int typeOnDemandThreshold) { + this.typeOnDemandThreshold = typeOnDemandThreshold; + return this; + } + + public Builder setStaticOnDemandThreshold(int staticOnDemandThreshold) { + this.staticOnDemandThreshold = staticOnDemandThreshold; + return this; + } + + public ImportRewriteConfiguration build() { + return new ImportRewriteConfiguration(this); + } + } + + final OriginalImportHandling originalImportHandling; + final ImportContainerSorting typeContainerSorting; + final ImportContainerSorting staticContainerSorting; + final ImplicitImportIdentification implicitImportIdentification; + final List importOrder; + final int typeOnDemandThreshold; + final int staticOnDemandThreshold; + + ImportRewriteConfiguration(Builder builder) { + this.originalImportHandling = builder.originalImportHandling; + this.typeContainerSorting = builder.typeContainerSorting; + this.staticContainerSorting = builder.staticContainerSorting; + this.implicitImportIdentification = builder.implicitImportIdentification; + this.importOrder = builder.importOrder; + this.typeOnDemandThreshold = builder.typeOnDemandThreshold; + this.staticOnDemandThreshold = builder.staticOnDemandThreshold; + } +} diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportsDelta.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportsDelta.java new file mode 100644 index 0000000..a630417 --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ImportsDelta.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Encapsulates a set of imports to add and a set of imports to remove. + */ +final class ImportsDelta { + final Set importsToAdd; + final Set importsToRemove; + + ImportsDelta(Collection importsToAdd, Collection importsToRemove) { + this.importsToAdd = Collections.unmodifiableSet(new HashSet(importsToAdd)); + this.importsToRemove = Collections.unmodifiableSet(new HashSet(importsToRemove)); + } + + @Override + public String toString() { + return String.format( + "(additions: %s, removals: %s)", //$NON-NLS-1$ + this.importsToAdd, + this.importsToRemove); + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/NewImportEntry.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/NewImportEntry.java new file mode 100644 index 0000000..8a5a929 --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/NewImportEntry.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +/** + * Represents an import declaration that did not originally occur in the compilation unit. + */ +class NewImportEntry extends ImportEntry { + NewImportEntry(ImportName importName) { + super(importName); + } + + @Override + public String toString() { + return String.format("NewImportEntry(%s)", this.importName); //$NON-NLS-1$ + } + + @Override + boolean isOriginal() { + return false; + } + + @Override + OriginalImportEntry asOriginalImportEntry() { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/OnDemandComputer.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/OnDemandComputer.java new file mode 100644 index 0000000..8b2d77d --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/OnDemandComputer.java @@ -0,0 +1,125 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +class OnDemandComputer { + private int typeOnDemandThreshold; + private int staticOnDemandThreshold; + + OnDemandComputer(int typeOnDemandThreshold, int staticOnDemandThreshold) { + this.typeOnDemandThreshold = typeOnDemandThreshold; + this.staticOnDemandThreshold = staticOnDemandThreshold; + } + + /** + * Identifies on-demand reductions (additions of on-demand imports with corresponding removal of + * single imports) satisfying the type and static on-demand import thresholds. + *

+ * Only containers to which imports have been added as part of the rewrite will be considered + * for on-demand reductions. + * + * @param imports + * the imports in the compilation unit + * @param addedImports + * the imports in the compilation unit which have been added as part of this rewrite, + * and whose containers should therefore be considered for on-demand reductions + * @param typeExplicitSimpleNames + * simple names of non-static single imports which must be preserved as single + * imports and not reduced into on-demand imports + * @param staticExplicitSimpleNames + * simple names of static single imports which must not be reduced into on-demand + * imports + */ + Collection identifyPossibleReductions( + Set imports, + Set addedImports, + Set typeExplicitSimpleNames, + Set staticExplicitSimpleNames) { + Collection candidates = new ArrayList(); + + Map> importsByContainer = mapByContainer(imports); + + for (Map.Entry> containerAndImports : importsByContainer.entrySet()) { + ImportName containerOnDemand = containerAndImports.getKey(); + Collection containerImports = containerAndImports.getValue(); + + Set explicitSimpleNames = + containerOnDemand.isStatic ? staticExplicitSimpleNames : typeExplicitSimpleNames; + + int onDemandThreshold = + containerOnDemand.isStatic ? this.staticOnDemandThreshold : this.typeOnDemandThreshold; + + OnDemandReduction candidate = maybeReduce( + containerOnDemand, containerImports, addedImports, onDemandThreshold, explicitSimpleNames); + if (candidate != null) { + candidates.add(candidate); + } + } + + return candidates; + } + + private Map> mapByContainer(Collection imports) { + Map> importsByContainer = new HashMap>(); + for (ImportName importName : imports) { + ImportName containerOnDemand = importName.getContainerOnDemand(); + + Collection containerImports = importsByContainer.get(containerOnDemand); + if (containerImports == null) { + containerImports = new ArrayList(); + importsByContainer.put(containerOnDemand, containerImports); + } + + containerImports.add(importName); + } + + return importsByContainer; + } + + private OnDemandReduction maybeReduce( + ImportName containerOnDemand, + Collection containerImports, + Set addedImports, + int onDemandThreshold, + Set explicitSimpleNames) { + boolean containerHasNewImport = false; + boolean containerHasOnDemand = false; + Collection reducibleImports = new ArrayList(); + + for (ImportName currentImport : containerImports) { + if (addedImports.contains(currentImport)) { + containerHasNewImport = true; + } + + if (currentImport.isOnDemand()) { + containerHasOnDemand = true; + } else { + if (!explicitSimpleNames.contains(currentImport.simpleName)) { + reducibleImports.add(currentImport); + } + } + } + + if (containerHasNewImport) { + if (containerHasOnDemand || reducibleImports.size() >= onDemandThreshold) { + return new OnDemandReduction(containerOnDemand, reducibleImports); + } + } + + return null; + } +} diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/OnDemandReduction.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/OnDemandReduction.java new file mode 100644 index 0000000..9be51f6 --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/OnDemandReduction.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +/** + * Indicates that one or more single imports ({@code reducibleImports}) are unnecessary or could be + * rendered unnecessary by the presence of an on-demand (.*) import ({@code containerOnDemand}) of + * their containing type or package. + *

+ * This "reduction" can be applied by removing all declarations of {@code reducibleImports} + * from the compilation unit and adding a declaration of {@code containerOnDemand} to the + * compilation unit if one is not already present. + */ +class OnDemandReduction { + final ImportName containerOnDemand; + final Collection reducibleImports; + + OnDemandReduction(ImportName containerName, Collection reducibleImports) { + this.containerOnDemand = containerName; + this.reducibleImports = Collections.unmodifiableCollection(new ArrayList(reducibleImports)); + } + + @Override + public String toString() { + return String.format("{%s: %s}", this.containerOnDemand.containerName, this.reducibleImports); //$NON-NLS-1$ + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/OrderPreservingImportAdder.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/OrderPreservingImportAdder.java new file mode 100644 index 0000000..398652e --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/OrderPreservingImportAdder.java @@ -0,0 +1,175 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.NavigableSet; +import java.util.TreeSet; + +/** + * Keeping existing imports in their existing order, inserts each new import before or after the + * import to which it would be adjacent if all (existing and new) imports were totally ordered + * together. + *

+ * A new import that would sort between two existing imports which are not adjacent in the + * existing order will be placed adjacent to the existing import with which it shares a longer + * prefix of dot-separated name segments. + */ +final class OrderPreservingImportAdder implements ImportAdder { + static class AdjacentImports { + final Collection importsBefore = new ArrayList(); + final Collection importsAfter = new ArrayList(); + + @Override + public String toString() { + return String.format("(%s, %s)", this.importsBefore.toString(), this.importsAfter.toString()); //$NON-NLS-1$ + } + } + + /** + * Returns the number of prefixing dot-separated segments shared between the two names. + *

+ * For example, {@code countMatchingPrefixSegments("foo.pack1.Class", "foo.pack2.Class")} will + * return 1 and {@code countMatchingPrefixSegments("foo.pack1.Class", "com.foo.pack1.Class")} + * will return 0. + */ + private static int countMatchingPrefixSegments(String name1, String name2) { + if (name1.isEmpty() || name2.isEmpty()) { + return 0; + } + + int matchingSegments = 0; + for (int i = 0; i <= name1.length() && i <= name2.length(); i++) { + boolean atEndOfName1Segment = i == name1.length() || name1.charAt(i) == '.'; + boolean atEndOfName2Segment = i == name2.length() || name2.charAt(i) == '.'; + if (atEndOfName1Segment && atEndOfName2Segment) { + matchingSegments++; + } else if (name1.charAt(i) != name2.charAt(i)) { + break; + } + } + + return matchingSegments; + } + + private final Comparator importComparator; + + OrderPreservingImportAdder(Comparator importComparator) { + this.importComparator = importComparator; + } + + @Override + public List addImports(Collection existingImports, Collection importsToAdd) { + if (importsToAdd.isEmpty()) { + return new ArrayList(existingImports); + } + + List sortedNewImports = new ArrayList(importsToAdd); + sortedNewImports.removeAll(new HashSet(existingImports)); + Collections.sort(sortedNewImports, this.importComparator); + + if (existingImports.isEmpty()) { + return sortedNewImports; + } + + Map adjacentNewImports = + determineAdjacentNewImports(new ArrayList(existingImports), sortedNewImports); + + List importsWithAdditions = + new ArrayList(existingImports.size() + sortedNewImports.size()); + for (ImportName existingImport : existingImports) { + // Remove the adjacent imports so they don't get inserted multiple times in the case + // of duplicate imports. + AdjacentImports adjacentImports = adjacentNewImports.remove(existingImport); + + if (adjacentImports != null) { + importsWithAdditions.addAll(adjacentImports.importsBefore); + } + + importsWithAdditions.add(existingImport); + + if (adjacentImports != null) { + importsWithAdditions.addAll(adjacentImports.importsAfter); + } + } + + return importsWithAdditions; + } + + /** + * Determines which new imports to place before and after each existing import. + *

+ * Returns a Map where each key is an existing import and each corresponding value is an + * AdjacentImports containing those new imports which should be placed before and after that + * existing import. Each new import will be placed either before or after exactly one existing + * import. + * + * @param existingImports + * Existing imports. + * @param sortedNewImports + * Imports to be added. Must be in order as if sorted by this.importComparator. + */ + private Map determineAdjacentNewImports( + Collection existingImports, + Iterable sortedNewImports) { + NavigableSet existingImportsTreeSet = new TreeSet(this.importComparator); + existingImportsTreeSet.addAll(existingImports); + + Map adjacentNewImports = new HashMap(); + for (ImportName existingImport : existingImports) { + adjacentNewImports.put(existingImport, new AdjacentImports()); + } + + for (ImportName newImport : sortedNewImports) { + ImportName precedingExistingImport = existingImportsTreeSet.lower(newImport); + ImportName succeedingExistingImport = existingImportsTreeSet.higher(newImport); + + if (shouldGroupWithSucceeding(newImport, precedingExistingImport, succeedingExistingImport)) { + adjacentNewImports.get(succeedingExistingImport).importsBefore.add(newImport); + } else { + adjacentNewImports.get(precedingExistingImport).importsAfter.add(newImport); + } + } + + return adjacentNewImports; + } + + /** + * Returns true if the new import should be placed before the existing import that would succeed + * it in sorted order, or false if the new import should be placed after the existing import + * that would precede it in sorted order. + */ + private boolean shouldGroupWithSucceeding( + ImportName newImport, ImportName precedingExistingImport, ImportName succeedingExistingImport) { + if (precedingExistingImport == null) { + return true; + } else if (succeedingExistingImport == null) { + return false; + } else { + String containerName = newImport.containerName; + + int prefixSharedWithPreceding = + countMatchingPrefixSegments(containerName, precedingExistingImport.containerName); + + int prefixSharedWithSucceeding = + countMatchingPrefixSegments(containerName, succeedingExistingImport.containerName); + + return prefixSharedWithSucceeding > prefixSharedWithPreceding; + } + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/OriginalImportEntry.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/OriginalImportEntry.java new file mode 100644 index 0000000..482a264 --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/OriginalImportEntry.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jface.text.IRegion; + +/** + * Represents an import declaration that originally occurred in the compilation unit. + */ +class OriginalImportEntry extends ImportEntry { + /** + * The comments associated with (either preceding, embedded within, or following) this import + * declaration. + */ + final List comments; + + /** + * The difference between the line number of the start of the import declaration (or the start + * of its first leading comment, if any) and the line number of the end of the preceding import + * declaration (or the end of that import's trailing comment, if any). Zero for the first import + * in the compilation unit. + */ + final int precedingLineDelimiters; + + /** + * The region of the compilation unit occupied by the whitespace (e.g. line delimiters) between + * the previous import (or its last trailing comment, if any) and this import declaration (or + * its first leading comment, if any). + */ + final IRegion leadingDelimiter; + + /** + * The region of the compilation unit occupied by the import declaration itself, its associated + * comments, and any whitespace between the import declaration and its comments. + */ + final IRegion declarationAndComments; + + OriginalImportEntry( + ImportName importName, + Collection comments, + int precedingLeadingDelimiters, + IRegion leadingWhitespace, + IRegion declarationAndComments) { + super(importName); + + this.comments = Collections.unmodifiableList(new ArrayList(comments)); + this.precedingLineDelimiters = precedingLeadingDelimiters; + this.leadingDelimiter = leadingWhitespace; + this.declarationAndComments = declarationAndComments; + } + + @Override + public String toString() { + return String.format("OriginalImportEntry(%s)", this.importName); //$NON-NLS-1$ + } + + @Override + boolean isOriginal() { + return true; + } + + @Override + OriginalImportEntry asOriginalImportEntry() { + return this; + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/PackageAndContainingTypeImportComparator.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/PackageAndContainingTypeImportComparator.java new file mode 100644 index 0000000..0f1b98d --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/PackageAndContainingTypeImportComparator.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import java.util.Comparator; + +/** + * Sorts imports according to a lexicographic comparison of their full container names (including + * package name and any containing type names). + *

+ * The alternative is {@link PackageImportComparator}. See https://bugs.eclipse.org/194358. + */ +final class PackageAndContainingTypeImportComparator implements Comparator { + @Override + public int compare(ImportName o1, ImportName o2) { + return o1.containerName.compareTo(o2.containerName); + } +} diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/PackageImportComparator.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/PackageImportComparator.java new file mode 100644 index 0000000..275d74d --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/PackageImportComparator.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import java.util.Comparator; + +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.Signature; +import org.eclipse.jdt.internal.core.JavaProject; + +/** + * Sorts imports according to a lexicographic comparison of their containing package names. + *

+ * This requires use of the JavaProject to look up packages and/or types by name in order to + * distinguish segments of the import's container name as containing package name vs. containing + * type name. + *

+ * The alternative is {@link PackageAndContainingTypeImportComparator}. See + * https://bugs.eclipse.org/194358. + */ +final class PackageImportComparator implements Comparator { + private final JavaProject javaProject; + + PackageImportComparator(JavaProject javaProject) { + this.javaProject = javaProject; + } + + @Override + public int compare(ImportName o1, ImportName o2) { + return determinePackageName(o1).compareTo(determinePackageName(o2)); + } + + private String determinePackageName(ImportName importName) { + String containerName = importName.containerName; + + try { + // Loop from longest to shortest prefix (of dot-separated name segments) of the + // container name until a package name is found. + String containerNamePrefix = containerName; + while (true) { + // Try to find a package named with this prefix. + if (this.javaProject.findPackageFragment(containerNamePrefix) != null) { + return containerNamePrefix; + } + + int lastSegmentStart = containerNamePrefix.lastIndexOf(Signature.C_DOT) + 1; + + // Use the heuristic that a prefix whose last segment starts with a lowercase letter + // is a package name, if we don't recognize the prefix as the name of a type. + if (this.javaProject.findType(containerNamePrefix) == null) { + if (Character.isLowerCase(containerNamePrefix.charAt(lastSegmentStart))) { + return containerNamePrefix; + } + } + + if (lastSegmentStart == 0) { + // No prefix of containerName could be resolved to a package name. + break; + } + + containerNamePrefix = containerNamePrefix.substring(0, lastSegmentStart - 1); + } + } catch (JavaModelException e) { + // An error occurred when we asked the JavaProject to resolve a name, + // so there is no point in proceeding with the loop. + } + + return containerName; + } +} diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/RemovedImportCommentReassigner.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/RemovedImportCommentReassigner.java new file mode 100644 index 0000000..39eff12 --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/RemovedImportCommentReassigner.java @@ -0,0 +1,186 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Reassigns comments associated with removed imports (those present before but not present + * after the rewrite) to resultant imports (those present after the rewrite). + *

+ * Reassigns comments of removed single imports to the first (in iteration order) resultant + * on-demand import having the same container name, if one exists. + *

+ * Reassigns comments of removed on-demand imports to the first (in iteration order) resultant + * single import having the same container name, if one exists. + *

+ * Leaves unassigned any removed import comment not matching the above cases. + */ +final class RemovedImportCommentReassigner { + private static Collection retainImportsWithComments(Collection imports) { + Collection importsWithComments = new ArrayList(imports.size()); + for (OriginalImportEntry currentImport : imports) { + if (!currentImport.comments.isEmpty()) { + importsWithComments.add(currentImport); + } + } + + return importsWithComments; + } + + private static boolean hasFloatingComment(OriginalImportEntry nextAssignedImport) { + for (ImportComment importComment : nextAssignedImport.comments) { + if (importComment.succeedingLineDelimiters > 1) { + return true; + } + } + + return false; + } + + private final Collection originalImportsWithComments; + + RemovedImportCommentReassigner(List originalImports) { + this.originalImportsWithComments = retainImportsWithComments(originalImports); + } + + /** + * Assigns comments of removed import entries (those in {@code originalImports} but not in + * {@code resultantImports}) to resultant import entries. + *

+ * Returns a map containing the resulting assignments, where each key is an element of + * {@code resultantImports} and each value is a collection of comments reassigned to that + * resultant import. + */ + Map> reassignComments(Collection resultantImports) { + Map> importAssignments = assignRemovedImports(resultantImports); + + Map> commentAssignments = + new HashMap>(); + + for (Map.Entry> importAssignment : importAssignments.entrySet()) { + ImportEntry targetImport = importAssignment.getKey(); + if (targetImport != null) { + Deque assignedComments = new ArrayDeque(); + + Collection assignedImports = importAssignment.getValue(); + + Iterator nextAssignedImportIterator = assignedImports.iterator(); + if (nextAssignedImportIterator.hasNext()) { + nextAssignedImportIterator.next(); + } + + Iterator assignedImportIterator = assignedImports.iterator(); + while (assignedImportIterator.hasNext()) { + OriginalImportEntry currentAssignedImport = assignedImportIterator.next(); + OriginalImportEntry nextAssignedImport = + nextAssignedImportIterator.hasNext() ? nextAssignedImportIterator.next() : null; + + assignedComments.addAll(currentAssignedImport.comments); + + if (nextAssignedImport != null && hasFloatingComment(nextAssignedImport)) { + // Ensure that a blank line separates this removed import's comments + // from the next removed import's floating comments. + ImportComment lastComment = assignedComments.removeLast(); + ImportComment lastCommentWithTrailingBlankLine = new ImportComment(lastComment.region, 2); + assignedComments.add(lastCommentWithTrailingBlankLine); + } + } + + commentAssignments.put(targetImport, assignedComments); + } + } + + return commentAssignments; + } + + private Map> assignRemovedImports(Collection imports) { + Collection removedImportsWithComments = identifyRemovedImportsWithComments(imports); + if (removedImportsWithComments.isEmpty()) { + return Collections.emptyMap(); + } + + Map firstSingleForOnDemand = identifyFirstSingleForEachOnDemand(imports); + Map firstOccurrences = identifyFirstOccurrenceOfEachImportName(imports); + + Map> removedImportsForRetainedImport = + new HashMap>(); + for (ImportEntry retainedImport : imports) { + removedImportsForRetainedImport.put(retainedImport, new ArrayList()); + } + // The null key will map to the removed imports not assigned to any import. + removedImportsForRetainedImport.put(null, new ArrayList()); + + for (OriginalImportEntry removedImport : removedImportsWithComments) { + ImportName removedImportName = removedImport.importName; + + final ImportEntry retainedImport; + if (removedImportName.isOnDemand()) { + retainedImport = firstSingleForOnDemand.get(removedImportName); + } else { + retainedImport = firstOccurrences.get(removedImportName.getContainerOnDemand()); + } + + // retainedImport will be null if there's no corresponding import to which to assign the removed import. + removedImportsForRetainedImport.get(retainedImport).add(removedImport); + } + + return removedImportsForRetainedImport; + } + + private Collection identifyRemovedImportsWithComments(Collection imports) { + Collection removedImports = + new ArrayList(this.originalImportsWithComments); + removedImports.removeAll(imports); + return removedImports; + } + + /** + * Assigns each removed on-demand import to the first single import in {@code imports} having + * the same container name. + *

+ * Returns a map where each key is a single import and each value is the corresponding + * removed on-demand import. + *

+ * The returned map only contains mappings to removed on-demand imports for which there are + * corresponding single imports in {@code imports}. + */ + private Map identifyFirstSingleForEachOnDemand(Iterable imports) { + Map firstSingleImportForContainer = new HashMap(); + for (ImportEntry currentImport : imports) { + if (!currentImport.importName.isOnDemand()) { + ImportName containerOnDemand = currentImport.importName.getContainerOnDemand(); + if (!firstSingleImportForContainer.containsKey(containerOnDemand)) { + firstSingleImportForContainer.put(containerOnDemand, currentImport); + } + } + } + return firstSingleImportForContainer; + } + + private Map identifyFirstOccurrenceOfEachImportName(Iterable imports) { + Map firstOccurrenceOfImport = new HashMap(); + for (ImportEntry resultantImport : imports) { + if (!firstOccurrenceOfImport.containsKey(resultantImport.importName)) { + firstOccurrenceOfImport.put(resultantImport.importName, resultantImport); + } + } + return firstOccurrenceOfImport; + } +} diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ReorderingImportAdder.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ReorderingImportAdder.java new file mode 100644 index 0000000..adc5e97 --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/ReorderingImportAdder.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Totally sorts new and existing imports together, discarding the order of existing imports. + */ +final class ReorderingImportAdder implements ImportAdder { + private final Comparator importComparator; + + ReorderingImportAdder(Comparator importComparator) { + this.importComparator = importComparator; + } + + @Override + public List addImports(Collection existingImports, Collection importsToAdd) { + Set existingImportsSet = new HashSet(existingImports); + + List importsWithAdditions = new ArrayList(existingImports.size() + importsToAdd.size()); + importsWithAdditions.addAll(existingImports); + for (ImportName importToAdd : importsToAdd) { + if (!existingImportsSet.contains(importToAdd)) { + importsWithAdditions.add(importToAdd); + } + } + + Collections.sort(importsWithAdditions, this.importComparator); + + return importsWithAdditions; + } +} diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/RewriteSite.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/RewriteSite.java new file mode 100644 index 0000000..715f152 --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/RewriteSite.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import org.eclipse.jface.text.IRegion; + +/** + * Describes the location in the compilation unit to be occupied by import declarations. + */ +class RewriteSite { + /** + * The region where import declarations, their associated comments, and any adjacent whitespace + * should be placed. + *

+ * Extends from the end of the AST node preceding import declarations and their comments (or the + * start of the compilation unit, if no such node exists) to the start of the AST node + * succeeding import declarations and their comments (or the end of the compilation unit, if no + * such node exists). + */ + final IRegion surroundingRegion; + + /** + * The region occupied by import declarations and their associated comments prior to the + * rewrite, or null if the compilation unit does not contain any import declarations. + *

+ * If not null, this region is contained within surroundingRegion. + */ + final IRegion importsRegion; + + /** + * True if the compilation unit prior to the rewrite contains any top-level AST nodes (package + * declaration and/or comments) preceding the start of surroundingRegion. + */ + final boolean hasPrecedingElements; + + /** + * True if the compilation unit prior to the rewrite contains any top-level AST nodes (type + * declarations and/or comments) following the end of surroundingRegion. + */ + final boolean hasSucceedingElements; + + RewriteSite( + IRegion surroundingRegion, + IRegion importsRegion, + boolean hasPrecedingElements, + boolean hasSucceedingElements) { + this.surroundingRegion = surroundingRegion; + this.importsRegion = importsRegion; + this.hasPrecedingElements = hasPrecedingElements; + this.hasSucceedingElements = hasSucceedingElements; + } +} diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/StaticConflictingSimpleNameFinder.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/StaticConflictingSimpleNameFinder.java new file mode 100644 index 0000000..d359d91 --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/StaticConflictingSimpleNameFinder.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.Flags; +import org.eclipse.jdt.core.IField; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaModelException; + +import java.util.HashSet; +import java.util.Set; + +/** + * Finds conflicts among importable static members declared within the specified on-demand-imported + * containers. + */ +final class StaticConflictingSimpleNameFinder implements ConflictingSimpleNameFinder { + private static boolean isStaticImportableMember(int memberFlags) { + return (Flags.isStatic(memberFlags) || Flags.isEnum(memberFlags)) && !Flags.isPrivate(memberFlags); + } + + private final IJavaProject project; + + StaticConflictingSimpleNameFinder(IJavaProject project) { + this.project = project; + } + + @Override + public Set findConflictingSimpleNames( + Set simpleNames, + Set onDemandAndImplicitContainerNames, + IProgressMonitor monitor) throws JavaModelException { + Set memberNamesFoundInMultipleTypes = new HashSet(); + + Set foundMemberNames = new HashSet(); + for (String containerName : onDemandAndImplicitContainerNames) { + IType containingType = this.project.findType(containerName, monitor); + if (containingType != null) { + for (String memberName : extractStaticMemberNames(containingType)) { + if (simpleNames.contains(memberName)) { + if (foundMemberNames.contains(memberName)) { + memberNamesFoundInMultipleTypes.add(memberName); + } else { + foundMemberNames.add(memberName); + } + } + } + } + } + + return memberNamesFoundInMultipleTypes; + } + + private Set extractStaticMemberNames(IType type) throws JavaModelException { + Set memberNames = new HashSet(); + + for (IField field : type.getFields()) { + if (isStaticImportableMember(field.getFlags())) { + memberNames.add(field.getElementName()); + } + } + + for (IMethod method : type.getMethods()) { + if (isStaticImportableMember(method.getFlags())) { + memberNames.add(method.getElementName()); + } + } + + return memberNames; + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/TypeConflictingSimpleNameFinder.java b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/TypeConflictingSimpleNameFinder.java new file mode 100644 index 0000000..2e96c96 --- /dev/null +++ b/org.eclipse.jdt.core/dom/org/eclipse/jdt/internal/core/dom/rewrite/imports/TypeConflictingSimpleNameFinder.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (c) 2015 Google Inc and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * John Glassmyer - import group sorting is broken - https://bugs.eclipse.org/430303 + *******************************************************************************/ +package org.eclipse.jdt.internal.core.dom.rewrite.imports; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.search.IJavaSearchConstants; +import org.eclipse.jdt.core.search.IJavaSearchScope; +import org.eclipse.jdt.core.search.SearchEngine; +import org.eclipse.jdt.core.search.TypeNameRequestor; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +final class TypeConflictingSimpleNameFinder implements ConflictingSimpleNameFinder { + private static class ConflictAccumulatingTypeRequestor extends TypeNameRequestor { + private static String buildContainerName(char[] packageName, char[][] enclosingTypeNames) { + StringBuffer buf= new StringBuffer(); + buf.append(packageName); + for (char[] enclosingTypeName : enclosingTypeNames) { + if (buf.length() > 0) + buf.append('.'); + buf.append(enclosingTypeName); + } + return buf.toString(); + } + + private final Set namesFoundInMultipleContainers; + private final Map lastContainerFoundForType; + + ConflictAccumulatingTypeRequestor() { + this.namesFoundInMultipleContainers = new HashSet(); + this.lastContainerFoundForType = new HashMap(); + } + + @Override + public void acceptType( + int modifiers, + char[] packageName, + char[] simpleTypeName, + char[][] enclosingTypeNames, + String path) { + String simpleName = new String(simpleTypeName); + String newContainerName = buildContainerName(packageName, enclosingTypeNames); + String oldContainerName = this.lastContainerFoundForType.put(simpleName, newContainerName); + if (oldContainerName != null && !oldContainerName.equals(newContainerName)) { + this.namesFoundInMultipleContainers.add(simpleName); + } + } + + Set getNamesFoundInMultipleContainers() { + return Collections.unmodifiableSet(this.namesFoundInMultipleContainers); + } + } + + private static char[][] stringsToCharArrays(Collection strings) { + char[][] arrayOfArrays = new char[strings.size()][]; + int i = 0; + for (String string : strings) { + arrayOfArrays[i] = string.toCharArray(); + i++; + } + return arrayOfArrays; + } + + private final IJavaProject javaProject; + private final SearchEngine searchEngine; + + TypeConflictingSimpleNameFinder(IJavaProject javaProject, SearchEngine searchEngine) { + this.javaProject = javaProject; + this.searchEngine = searchEngine; + } + + @Override + public Set findConflictingSimpleNames( + Set simpleNames, + Set onDemandAndImplicitContainerNames, + IProgressMonitor monitor) throws JavaModelException { + IJavaSearchScope scope = SearchEngine.createJavaSearchScope(new IJavaElement[] { this.javaProject }); + + ConflictAccumulatingTypeRequestor requestor = new ConflictAccumulatingTypeRequestor(); + + this.searchEngine.searchAllTypeNames( + stringsToCharArrays(onDemandAndImplicitContainerNames), + stringsToCharArrays(simpleNames), + scope, + requestor, + IJavaSearchConstants.WAIT_UNTIL_READY_TO_SEARCH, + monitor); + + return requestor.getNamesFoundInMultipleContainers(); + } +} \ No newline at end of file