diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint')
34 files changed, 8097 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddPrefixFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddPrefixFix.java new file mode 100644 index 000000000..d8ce657db --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddPrefixFix.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import static com.android.SdkConstants.ANDROID_URI; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.utils.XmlUtils; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.w3c.dom.Node; + +@SuppressWarnings("restriction") // DOM model +final class AddPrefixFix extends DocumentFix { + private AddPrefixFix(String id, IMarker marker) { + super(id, marker); + } + + @Override + public boolean needsFocus() { + return false; + } + + @Override + public boolean isCancelable() { + return false; + } + + @Override + protected void apply(IDocument document, IStructuredModel model, Node node, int start, + int end) { + String prefix = XmlUtils.lookupNamespacePrefix(node, ANDROID_URI); + try { + document.replace(start, 0, prefix + ':'); + } catch (BadLocationException e) { + AdtPlugin.log(e, null); + } + } + + @Override + public String getDisplayString() { + return "Add in an Android namespace prefix"; + } + + @Override + public Image getImage() { + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + return sharedImages.getImage(ISharedImages.IMG_OBJ_ADD); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAnnotation.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAnnotation.java new file mode 100644 index 000000000..1a7fe5697 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAnnotation.java @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.lint; + +import static com.android.SdkConstants.FQCN_SUPPRESS_LINT; +import static com.android.SdkConstants.FQCN_TARGET_API; +import static com.android.SdkConstants.SUPPRESS_LINT; +import static com.android.SdkConstants.TARGET_API; +import static org.eclipse.jdt.core.dom.ArrayInitializer.EXPRESSIONS_PROPERTY; +import static org.eclipse.jdt.core.dom.SingleMemberAnnotation.VALUE_PROPERTY; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.SdkVersionInfo; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.tools.lint.checks.AnnotationDetector; +import com.android.tools.lint.checks.ApiDetector; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.Scope; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.AnonymousClassDeclaration; +import org.eclipse.jdt.core.dom.ArrayInitializer; +import org.eclipse.jdt.core.dom.BodyDeclaration; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.FieldDeclaration; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.NodeFinder; +import org.eclipse.jdt.core.dom.SingleMemberAnnotation; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; +import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; +import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; +import org.eclipse.jdt.core.dom.rewrite.ListRewrite; +import org.eclipse.jdt.ui.IWorkingCopyManager; +import org.eclipse.jdt.ui.JavaUI; +import org.eclipse.jdt.ui.SharedASTProvider; +import org.eclipse.jface.text.IDocument; +import org.eclipse.swt.graphics.Image; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.TextEdit; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IMarkerResolution; +import org.eclipse.ui.IMarkerResolution2; +import org.eclipse.ui.texteditor.IDocumentProvider; +import org.eclipse.ui.texteditor.ITextEditor; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Marker resolution for adding {@code @SuppressLint} annotations in Java files. + * It can also add {@code @TargetApi} annotations. + */ +class AddSuppressAnnotation implements IMarkerResolution2 { + private final IMarker mMarker; + private final String mId; + private final BodyDeclaration mNode; + private final String mDescription; + /** + * Should it create a {@code @TargetApi} annotation instead of + * {@code SuppressLint} ? If so pass a non null API level + */ + private final String mTargetApi; + + private AddSuppressAnnotation( + @NonNull String id, + @NonNull IMarker marker, + @NonNull BodyDeclaration node, + @NonNull String description, + @Nullable String targetApi) { + mId = id; + mMarker = marker; + mNode = node; + mDescription = description; + mTargetApi = targetApi; + } + + @Override + public String getLabel() { + return mDescription; + } + + @Override + public String getDescription() { + return null; + } + + @Override + public Image getImage() { + return IconFactory.getInstance().getIcon("newannotation"); //$NON-NLS-1$ + } + + @Override + public void run(IMarker marker) { + ITextEditor textEditor = AdtUtils.getActiveTextEditor(); + IDocumentProvider provider = textEditor.getDocumentProvider(); + IEditorInput editorInput = textEditor.getEditorInput(); + IDocument document = provider.getDocument(editorInput); + if (document == null) { + return; + } + IWorkingCopyManager manager = JavaUI.getWorkingCopyManager(); + ICompilationUnit compilationUnit = manager.getWorkingCopy(editorInput); + try { + MultiTextEdit edit; + if (mTargetApi == null) { + edit = addSuppressAnnotation(document, compilationUnit, mNode); + } else { + edit = addTargetApiAnnotation(document, compilationUnit, mNode); + } + if (edit != null) { + edit.apply(document); + + // Remove the marker now that the suppress annotation has been added + // (so the user doesn't have to re-run lint just to see it disappear, + // and besides we don't want to keep offering marker resolutions on this + // marker which could lead to duplicate annotations since the above code + // assumes that the current id isn't in the list of values, since otherwise + // lint shouldn't have complained here. + mMarker.delete(); + } + } catch (Exception ex) { + AdtPlugin.log(ex, "Could not add suppress annotation"); + } + } + + @SuppressWarnings({"rawtypes"}) // Java AST API has raw types + private MultiTextEdit addSuppressAnnotation( + IDocument document, + ICompilationUnit compilationUnit, + BodyDeclaration declaration) throws CoreException { + List modifiers = declaration.modifiers(); + SingleMemberAnnotation existing = null; + for (Object o : modifiers) { + if (o instanceof SingleMemberAnnotation) { + SingleMemberAnnotation annotation = (SingleMemberAnnotation) o; + String type = annotation.getTypeName().getFullyQualifiedName(); + if (type.equals(FQCN_SUPPRESS_LINT) || type.endsWith(SUPPRESS_LINT)) { + existing = annotation; + break; + } + } + } + + ImportRewrite importRewrite = ImportRewrite.create(compilationUnit, true); + String local = importRewrite.addImport(FQCN_SUPPRESS_LINT); + AST ast = declaration.getAST(); + ASTRewrite rewriter = ASTRewrite.create(ast); + if (existing == null) { + SingleMemberAnnotation newAnnotation = ast.newSingleMemberAnnotation(); + newAnnotation.setTypeName(ast.newSimpleName(local)); + StringLiteral value = ast.newStringLiteral(); + value.setLiteralValue(mId); + newAnnotation.setValue(value); + ListRewrite listRewrite = rewriter.getListRewrite(declaration, + declaration.getModifiersProperty()); + listRewrite.insertFirst(newAnnotation, null); + } else { + Expression existingValue = existing.getValue(); + if (existingValue instanceof StringLiteral) { + StringLiteral stringLiteral = (StringLiteral) existingValue; + if (mId.equals(stringLiteral.getLiteralValue())) { + // Already contains the id + return null; + } + // Create a new array initializer holding the old string plus the new id + ArrayInitializer array = ast.newArrayInitializer(); + StringLiteral old = ast.newStringLiteral(); + old.setLiteralValue(stringLiteral.getLiteralValue()); + array.expressions().add(old); + StringLiteral value = ast.newStringLiteral(); + value.setLiteralValue(mId); + array.expressions().add(value); + rewriter.set(existing, VALUE_PROPERTY, array, null); + } else if (existingValue instanceof ArrayInitializer) { + // Existing array: just append the new string + ArrayInitializer array = (ArrayInitializer) existingValue; + List expressions = array.expressions(); + if (expressions != null) { + for (Object o : expressions) { + if (o instanceof StringLiteral) { + if (mId.equals(((StringLiteral)o).getLiteralValue())) { + // Already contains the id + return null; + } + } + } + } + StringLiteral value = ast.newStringLiteral(); + value.setLiteralValue(mId); + ListRewrite listRewrite = rewriter.getListRewrite(array, EXPRESSIONS_PROPERTY); + listRewrite.insertLast(value, null); + } else { + assert false : existingValue; + return null; + } + } + + TextEdit importEdits = importRewrite.rewriteImports(new NullProgressMonitor()); + TextEdit annotationEdits = rewriter.rewriteAST(document, null); + + // Apply to the document + MultiTextEdit edit = new MultiTextEdit(); + // Create the edit to change the imports, only if + // anything changed + if (importEdits.hasChildren()) { + edit.addChild(importEdits); + } + edit.addChild(annotationEdits); + + return edit; + } + + @SuppressWarnings({"rawtypes"}) // Java AST API has raw types + private MultiTextEdit addTargetApiAnnotation( + IDocument document, + ICompilationUnit compilationUnit, + BodyDeclaration declaration) throws CoreException { + List modifiers = declaration.modifiers(); + SingleMemberAnnotation existing = null; + for (Object o : modifiers) { + if (o instanceof SingleMemberAnnotation) { + SingleMemberAnnotation annotation = (SingleMemberAnnotation) o; + String type = annotation.getTypeName().getFullyQualifiedName(); + if (type.equals(FQCN_TARGET_API) || type.endsWith(TARGET_API)) { + existing = annotation; + break; + } + } + } + + ImportRewrite importRewrite = ImportRewrite.create(compilationUnit, true); + importRewrite.addImport("android.os.Build"); //$NON-NLS-1$ + String local = importRewrite.addImport(FQCN_TARGET_API); + AST ast = declaration.getAST(); + ASTRewrite rewriter = ASTRewrite.create(ast); + if (existing == null) { + SingleMemberAnnotation newAnnotation = ast.newSingleMemberAnnotation(); + newAnnotation.setTypeName(ast.newSimpleName(local)); + Expression value = createLiteral(ast); + newAnnotation.setValue(value); + ListRewrite listRewrite = rewriter.getListRewrite(declaration, + declaration.getModifiersProperty()); + listRewrite.insertFirst(newAnnotation, null); + } else { + Expression value = createLiteral(ast); + rewriter.set(existing, VALUE_PROPERTY, value, null); + } + + TextEdit importEdits = importRewrite.rewriteImports(new NullProgressMonitor()); + TextEdit annotationEdits = rewriter.rewriteAST(document, null); + MultiTextEdit edit = new MultiTextEdit(); + if (importEdits.hasChildren()) { + edit.addChild(importEdits); + } + edit.addChild(annotationEdits); + + return edit; + } + + private Expression createLiteral(AST ast) { + Expression value; + if (!isCodeName()) { + value = ast.newQualifiedName( + ast.newQualifiedName(ast.newSimpleName("Build"), //$NON-NLS-1$ + ast.newSimpleName("VERSION_CODES")), //$NON-NLS-1$ + ast.newSimpleName(mTargetApi)); + } else { + value = ast.newNumberLiteral(mTargetApi); + } + return value; + } + + private boolean isCodeName() { + return Character.isDigit(mTargetApi.charAt(0)); + } + + /** + * Adds any applicable suppress lint fix resolutions into the given list + * + * @param marker the marker to create fixes for + * @param id the issue id + * @param resolutions a list to add the created resolutions into, if any + */ + public static void createFixes(IMarker marker, String id, + List<IMarkerResolution> resolutions) { + ITextEditor textEditor = AdtUtils.getActiveTextEditor(); + IDocumentProvider provider = textEditor.getDocumentProvider(); + IEditorInput editorInput = textEditor.getEditorInput(); + IDocument document = provider.getDocument(editorInput); + if (document == null) { + return; + } + + IWorkingCopyManager manager = JavaUI.getWorkingCopyManager(); + ICompilationUnit compilationUnit = manager.getWorkingCopy(editorInput); + int offset = 0; + int length = 0; + int start = marker.getAttribute(IMarker.CHAR_START, -1); + int end = marker.getAttribute(IMarker.CHAR_END, -1); + offset = start; + length = end - start; + CompilationUnit root = SharedASTProvider.getAST(compilationUnit, + SharedASTProvider.WAIT_YES, null); + if (root == null) { + return; + } + + int api = -1; + if (id.equals(ApiDetector.UNSUPPORTED.getId()) || + id.equals(ApiDetector.INLINED.getId())) { + String message = marker.getAttribute(IMarker.MESSAGE, null); + if (message != null) { + Pattern pattern = Pattern.compile("\\s(\\d+)\\s"); //$NON-NLS-1$ + Matcher matcher = pattern.matcher(message); + if (matcher.find()) { + api = Integer.parseInt(matcher.group(1)); + } + } + } + + Issue issue = EclipseLintClient.getRegistry().getIssue(id); + boolean isClassDetector = issue != null && issue.getImplementation().getScope().contains( + Scope.CLASS_FILE); + + // Don't offer to suppress (with an annotation) the annotation checks + if (issue == AnnotationDetector.ISSUE) { + return; + } + + NodeFinder nodeFinder = new NodeFinder(root, offset, length); + ASTNode coveringNode; + if (offset <= 0) { + // Error added on the first line of a Java class: typically from a class-based + // detector which lacks line information. Map this to the top level class + // in the file instead. + coveringNode = root; + if (root.types() != null && root.types().size() > 0) { + Object type = root.types().get(0); + if (type instanceof ASTNode) { + coveringNode = (ASTNode) type; + } + } + } else { + coveringNode = nodeFinder.getCoveringNode(); + } + for (ASTNode body = coveringNode; body != null; body = body.getParent()) { + if (body instanceof BodyDeclaration) { + BodyDeclaration declaration = (BodyDeclaration) body; + + String target = null; + if (body instanceof MethodDeclaration) { + target = ((MethodDeclaration) body).getName().toString() + "()"; //$NON-NLS-1$ + } else if (body instanceof FieldDeclaration) { + target = "field"; + FieldDeclaration field = (FieldDeclaration) body; + if (field.fragments() != null && field.fragments().size() > 0) { + ASTNode first = (ASTNode) field.fragments().get(0); + if (first instanceof VariableDeclarationFragment) { + VariableDeclarationFragment decl = (VariableDeclarationFragment) first; + target = decl.getName().toString(); + } + } + } else if (body instanceof AnonymousClassDeclaration) { + target = "anonymous class"; + } else if (body instanceof TypeDeclaration) { + target = ((TypeDeclaration) body).getName().toString(); + } else { + target = body.getClass().getSimpleName(); + } + + // In class files, detectors can only find annotations on methods + // and on classes, not on variable declarations + if (isClassDetector && !(body instanceof MethodDeclaration + || body instanceof TypeDeclaration + || body instanceof AnonymousClassDeclaration + || body instanceof FieldDeclaration)) { + continue; + } + + String desc = String.format("Add @SuppressLint '%1$s\' to '%2$s'", id, target); + resolutions.add(new AddSuppressAnnotation(id, marker, declaration, desc, null)); + + if (api != -1 + // @TargetApi is only valid on methods and classes, not fields etc + && (body instanceof MethodDeclaration + || body instanceof TypeDeclaration)) { + String apiString = SdkVersionInfo.getBuildCode(api); + if (apiString == null) { + apiString = Integer.toString(api); + } + desc = String.format("Add @TargetApi(%1$s) to '%2$s'", apiString, target); + resolutions.add(new AddSuppressAnnotation(id, marker, declaration, desc, + apiString)); + } + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java new file mode 100644 index 000000000..88e0880e5 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.lint; + +import static com.android.SdkConstants.ATTR_IGNORE; +import static com.android.SdkConstants.ATTR_TARGET_API; +import static com.android.SdkConstants.DOT_XML; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.SdkVersionInfo; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.tools.lint.checks.ApiDetector; +import com.google.common.collect.Lists; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.contentassist.ICompletionProposal; +import org.eclipse.jface.text.contentassist.IContextInformation; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Fix for adding {@code tools:ignore="id"} attributes in XML files. + */ +class AddSuppressAttribute implements ICompletionProposal { + private final AndroidXmlEditor mEditor; + private final String mId; + private final IMarker mMarker; + private final Element mElement; + private final String mDescription; + /** + * Should it create a {@code tools:targetApi} attribute instead of a + * {@code tools:ignore} attribute? If so pass a non null API level + */ + private final String mTargetApi; + + + private AddSuppressAttribute( + @NonNull AndroidXmlEditor editor, + @NonNull String id, + @NonNull IMarker marker, + @NonNull Element element, + @NonNull String description, + @Nullable String targetApi) { + mEditor = editor; + mId = id; + mMarker = marker; + mElement = element; + mDescription = description; + mTargetApi = targetApi; + } + + @Override + public Point getSelection(IDocument document) { + return null; + } + + @Override + public String getAdditionalProposalInfo() { + return null; + } + + @Override + public String getDisplayString() { + return mDescription; + } + + @Override + public IContextInformation getContextInformation() { + return null; + } + + @Override + public Image getImage() { + return IconFactory.getInstance().getIcon("newannotation"); //$NON-NLS-1$ + } + + @Override + public void apply(IDocument document) { + String attribute; + String value; + if (mTargetApi != null) { + attribute = ATTR_TARGET_API; + value = mTargetApi; + } else { + attribute = ATTR_IGNORE; + value = mId; + } + AdtUtils.setToolsAttribute(mEditor, mElement, mDescription, attribute, value, + true /*reveal*/, true /*append*/); + + try { + // Remove the marker now that the suppress attribute has been added + // (so the user doesn't have to re-run lint just to see it disappear) + mMarker.delete(); + } catch (CoreException e) { + AdtPlugin.log(e, "Could not remove marker"); + } + } + + /** + * Returns a quickfix to suppress a specific lint issue id on the node corresponding to + * the given marker. + * + * @param editor the associated editor containing the marker + * @param marker the marker to create fixes for + * @param id the issue id + * @return a list of fixes for this marker, possibly empty + */ + @NonNull + public static List<AddSuppressAttribute> createFixes( + @NonNull AndroidXmlEditor editor, + @NonNull IMarker marker, + @NonNull String id) { + // This only applies to XML files: + String fileName = marker.getResource().getName(); + if (!fileName.endsWith(DOT_XML)) { + return Collections.emptyList(); + } + + int offset = marker.getAttribute(IMarker.CHAR_START, -1); + Node node; + if (offset == -1) { + node = DomUtilities.getNode(editor.getStructuredDocument(), 0); + if (node != null) { + node = node.getOwnerDocument().getDocumentElement(); + } + } else { + node = DomUtilities.getNode(editor.getStructuredDocument(), offset); + } + if (node == null) { + return Collections.emptyList(); + } + Document document = node.getOwnerDocument(); + while (node != null && node.getNodeType() != Node.ELEMENT_NODE) { + node = node.getParentNode(); + } + if (node == null) { + node = document.getDocumentElement(); + if (node == null) { + return Collections.emptyList(); + } + } + + String desc = String.format("Add ignore '%1$s\' to element", id); + Element element = (Element) node; + List<AddSuppressAttribute> fixes = Lists.newArrayListWithExpectedSize(2); + fixes.add(new AddSuppressAttribute(editor, id, marker, element, desc, null)); + + int api = -1; + if (id.equals(ApiDetector.UNSUPPORTED.getId()) + || id.equals(ApiDetector.INLINED.getId())) { + String message = marker.getAttribute(IMarker.MESSAGE, null); + if (message != null) { + Pattern pattern = Pattern.compile("\\s(\\d+)\\s"); //$NON-NLS-1$ + Matcher matcher = pattern.matcher(message); + if (matcher.find()) { + api = Integer.parseInt(matcher.group(1)); + String targetApi; + String buildCode = SdkVersionInfo.getBuildCode(api); + if (buildCode != null) { + targetApi = buildCode.toLowerCase(Locale.US); + fixes.add(new AddSuppressAttribute(editor, id, marker, element, + String.format("Add targetApi '%1$s\' to element", targetApi), + targetApi)); + } + targetApi = Integer.toString(api); + fixes.add(new AddSuppressAttribute(editor, id, marker, element, + String.format("Add targetApi '%1$s\' to element", targetApi), + targetApi)); + } + } + } + + return fixes; + } + + /** + * Returns a quickfix to suppress a given issue type on the <b>root element</b> + * of the given editor. + * + * @param editor the associated editor containing the marker + * @param marker the marker to create fixes for + * @param id the issue id + * @return a fix for this marker, or null if unable + */ + @Nullable + public static AddSuppressAttribute createFixForAll( + @NonNull AndroidXmlEditor editor, + @NonNull IMarker marker, + @NonNull String id) { + // This only applies to XML files: + String fileName = marker.getResource().getName(); + if (!fileName.endsWith(DOT_XML)) { + return null; + } + + Node node = DomUtilities.getNode(editor.getStructuredDocument(), 0); + if (node != null) { + node = node.getOwnerDocument().getDocumentElement(); + String desc = String.format("Add ignore '%1$s\' to element", id); + Element element = (Element) node; + return new AddSuppressAttribute(editor, id, marker, element, desc, null); + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ClearLintMarkersAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ClearLintMarkersAction.java new file mode 100644 index 000000000..a10d39472 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ClearLintMarkersAction.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.lint; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.ui.IActionDelegate; + +import java.util.List; + +/** Action which clear lint markers from the current project */ +public class ClearLintMarkersAction implements IActionDelegate { + + private ISelection mSelection; + + @Override + public void selectionChanged(IAction action, ISelection selection) { + mSelection = selection; + } + + @Override + public void run(IAction action) { + List<IProject> projects = RunLintAction.getProjects(mSelection, false /*warn*/); + if (projects != null) { + EclipseLintRunner.cancelCurrentJobs(false); + EclipseLintClient.clearMarkers(projects); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ColumnDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ColumnDialog.java new file mode 100644 index 000000000..be987d498 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ColumnDialog.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import com.android.ide.eclipse.adt.AdtPlugin; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.viewers.CheckboxTableViewer; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Table; +import org.eclipse.ui.dialogs.SelectionStatusDialog; + +/** + * Dialog for editing visible columns in the {@link LintList} + */ +class ColumnDialog extends SelectionStatusDialog implements Listener, IStructuredContentProvider { + private LintColumn[] mColumns; + private LintColumn[] mSelectedColumns; + private CheckboxTableViewer mViewer; + + public ColumnDialog(Shell parent, LintColumn[] fields, LintColumn[] selected) { + super(parent); + mColumns = fields; + mSelectedColumns = selected; + setTitle("Select Visible Columns"); + setHelpAvailable(false); + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite container = new Composite(parent, SWT.NONE); + container.setLayout(new GridLayout(1, false)); + GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1); + // Wide enough to accommodate the error label + gridData.widthHint = 500; + container.setLayoutData(gridData); + + Label lblSelectVisibleColumns = new Label(container, SWT.NONE); + lblSelectVisibleColumns.setText("Select visible columns:"); + + mViewer = CheckboxTableViewer.newCheckList(container, + SWT.BORDER | SWT.FULL_SELECTION | SWT.HIDE_SELECTION); + Table table = mViewer.getTable(); + table.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); + mViewer.setContentProvider(this); + + mViewer.setInput(mColumns); + mViewer.setCheckedElements(mSelectedColumns); + + validate(); + + return container; + } + + @Override + protected void computeResult() { + Object[] checked = mViewer.getCheckedElements(); + mSelectedColumns = new LintColumn[checked.length]; + for (int i = 0, n = checked.length; i < n; i++) { + mSelectedColumns[i] = (LintColumn) checked[i]; + } + } + + public LintColumn[] getSelectedColumns() { + return mSelectedColumns; + } + + @Override + public void handleEvent(Event event) { + validate(); + } + + private void validate() { + IStatus status; + computeResult(); + + if (mViewer.getCheckedElements().length <= 1) { + status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "Must selected at least one column"); + } else { + status = new Status(IStatus.OK, AdtPlugin.PLUGIN_ID, null); + } + updateStatus(status); + } + + // ---- Implements IStructuredContentProvider ---- + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + @Override + public Object[] getElements(Object inputElement) { + return mColumns; + } + }
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ConvertToDpFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ConvertToDpFix.java new file mode 100644 index 000000000..628972f8c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ConvertToDpFix.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import static com.android.SdkConstants.UNIT_PX; +import static com.android.SdkConstants.VALUE_N_DP; + +import com.android.ide.eclipse.adt.AdtPlugin; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.dialogs.IInputValidator; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@SuppressWarnings("restriction") // DOM model +final class ConvertToDpFix extends DocumentFix implements IInputValidator { + private ConvertToDpFix(String id, IMarker marker) { + super(id, marker); + } + + @Override + public boolean needsFocus() { + return false; + } + + @Override + public boolean isCancelable() { + return true; + } + + @Override + protected void apply(IDocument document, IStructuredModel model, Node node, int start, + int end) { + Shell shell = AdtPlugin.getShell(); + InputDensityDialog densityDialog = new InputDensityDialog(shell); + if (densityDialog.open() == Window.OK) { + int dpi = densityDialog.getDensity(); + Element element = (Element) node; + Pattern pattern = Pattern.compile("(\\d+)px"); //$NON-NLS-1$ + NamedNodeMap attributes = element.getAttributes(); + for (int i = 0, n = attributes.getLength(); i < n; i++) { + Attr attribute = (Attr) attributes.item(i); + String value = attribute.getValue(); + if (value.endsWith(UNIT_PX)) { + Matcher matcher = pattern.matcher(value); + if (matcher.matches()) { + String numberString = matcher.group(1); + try { + int px = Integer.parseInt(numberString); + int dp = px * 160 / dpi; + String newValue = String.format(VALUE_N_DP, dp); + attribute.setNodeValue(newValue); + } catch (NumberFormatException nufe) { + AdtPlugin.log(nufe, null); + } + } + } + } + } + } + + @Override + public String getDisplayString() { + return "Convert to \"dp\"..."; + } + + @Override + public Image getImage() { + return AdtPlugin.getAndroidLogo(); + } + + // ---- Implements IInputValidator ---- + + @Override + public String isValid(String input) { + if (input == null || input.length() == 0) + return " "; //$NON-NLS-1$ + + try { + int i = Integer.parseInt(input); + if (i <= 0 || i > 1000) { + return "Invalid range"; + } + } catch (NumberFormatException x) { + return "Enter a valid number"; + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/DocumentFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/DocumentFix.java new file mode 100644 index 000000000..e17d5ec97 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/DocumentFix.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.text.IDocument; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; +import org.w3c.dom.Node; + +@SuppressWarnings("restriction") // DOM model +abstract class DocumentFix extends LintFix { + + protected DocumentFix(String id, IMarker marker) { + super(id, marker); + } + + protected abstract void apply(IDocument document, IStructuredModel model, Node node, + int start, int end); + + @Override + public void apply(IDocument document) { + if (!(document instanceof IStructuredDocument)) { + AdtPlugin.log(null, "Unexpected document type: %1$s. Can't fix.", + document.getClass().getName()); + return; + } + int start = mMarker.getAttribute(IMarker.CHAR_START, -1); + int end = mMarker.getAttribute(IMarker.CHAR_END, -1); + if (start != -1 && end != -1) { + IModelManager manager = StructuredModelManager.getModelManager(); + IStructuredModel model = manager.getModelForEdit((IStructuredDocument) document); + Node node = DomUtilities.getNode(document, start); + try { + apply(document, model, node, start, end); + } finally { + model.releaseFromEdit(); + } + + if (!isCancelable()) { + deleteMarker(); + } + } + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/DosLineEndingsFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/DosLineEndingsFix.java new file mode 100644 index 000000000..9a5456b56 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/DosLineEndingsFix.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import com.android.ide.eclipse.adt.AdtPlugin; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; + +/** Quickfix for correcting line endings in the file */ +class DosLineEndingsFix extends LintFix { + + protected DosLineEndingsFix(String id, IMarker marker) { + super(id, marker); + } + + @Override + public boolean needsFocus() { + return false; + } + + @Override + public boolean isCancelable() { + return false; + } + + @Override + public String getDisplayString() { + return "Fix line endings"; + } + + @Override + public void apply(IDocument document) { + char next = 0; + for (int i = document.getLength() - 1; i >= 0; i--) { + try { + char c = document.getChar(i); + if (c == '\r' && next != '\n') { + document.replace(i, 1, "\n"); //$NON-NLS-1$ + } + next = c; + } catch (BadLocationException e) { + AdtPlugin.log(e, null); + return; + } + } + + deleteMarker(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java new file mode 100644 index 000000000..3dd424087 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java @@ -0,0 +1,1306 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import static com.android.SdkConstants.DOT_JAR; +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.FD_NATIVE_LIBS; +import static com.android.ide.eclipse.adt.AdtConstants.MARKER_LINT; +import static com.android.ide.eclipse.adt.AdtUtils.workspacePathToFile; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.sdklib.IAndroidTarget; +import com.android.tools.lint.checks.BuiltinIssueRegistry; +import com.android.tools.lint.client.api.Configuration; +import com.android.tools.lint.client.api.IssueRegistry; +import com.android.tools.lint.client.api.JavaParser; +import com.android.tools.lint.client.api.LintClient; +import com.android.tools.lint.client.api.LintDriver; +import com.android.tools.lint.client.api.XmlParser; +import com.android.tools.lint.detector.api.ClassContext; +import com.android.tools.lint.detector.api.Context; +import com.android.tools.lint.detector.api.DefaultPosition; +import com.android.tools.lint.detector.api.Detector; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.JavaContext; +import com.android.tools.lint.detector.api.LintUtils; +import com.android.tools.lint.detector.api.Location; +import com.android.tools.lint.detector.api.Location.Handle; +import com.android.tools.lint.detector.api.Position; +import com.android.tools.lint.detector.api.Project; +import com.android.tools.lint.detector.api.Severity; +import com.android.tools.lint.detector.api.TextFormat; +import com.android.tools.lint.detector.api.XmlContext; +import com.android.utils.Pair; +import com.android.utils.SdkUtils; +import com.google.common.collect.Maps; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeHierarchy; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.internal.compiler.CompilationResult; +import org.eclipse.jdt.internal.compiler.DefaultErrorHandlingPolicies; +import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration; +import org.eclipse.jdt.internal.compiler.batch.CompilationUnit; +import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; +import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; +import org.eclipse.jdt.internal.compiler.parser.Parser; +import org.eclipse.jdt.internal.compiler.problem.AbortCompilation; +import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory; +import org.eclipse.jdt.internal.compiler.problem.ProblemReporter; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.editors.text.TextFileDocumentProvider; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.texteditor.IDocumentProvider; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; +import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; + +import lombok.ast.ecj.EcjTreeConverter; +import lombok.ast.grammar.ParseProblem; +import lombok.ast.grammar.Source; + +/** + * Eclipse implementation for running lint on workspace files and projects. + */ +@SuppressWarnings("restriction") // DOM model +public class EclipseLintClient extends LintClient { + static final String MARKER_CHECKID_PROPERTY = "checkid"; //$NON-NLS-1$ + private static final String MODEL_PROPERTY = "model"; //$NON-NLS-1$ + private final List<? extends IResource> mResources; + private final IDocument mDocument; + private boolean mWasFatal; + private boolean mFatalOnly; + private EclipseJavaParser mJavaParser; + private boolean mCollectNodes; + private Map<Node, IMarker> mNodeMap; + + /** + * Creates a new {@link EclipseLintClient}. + * + * @param registry the associated detector registry + * @param resources the associated resources (project, file or null) + * @param document the associated document, or null if the {@code resource} + * param is not a file + * @param fatalOnly whether only fatal issues should be reported (and therefore checked) + */ + public EclipseLintClient(IssueRegistry registry, List<? extends IResource> resources, + IDocument document, boolean fatalOnly) { + mResources = resources; + mDocument = document; + mFatalOnly = fatalOnly; + } + + /** + * Returns true if lint should only check fatal issues + * + * @return true if lint should only check fatal issues + */ + public boolean isFatalOnly() { + return mFatalOnly; + } + + /** + * Sets whether the lint client should store associated XML nodes for each + * reported issue + * + * @param collectNodes if true, collect node positions for errors in XML + * files, retrievable via the {@link #getIssueForNode} method + */ + public void setCollectNodes(boolean collectNodes) { + mCollectNodes = collectNodes; + } + + /** + * Returns one of the issues for the given node (there could be more than one) + * + * @param node the node to look up lint issues for + * @return the marker for one of the issues found for the given node + */ + @Nullable + public IMarker getIssueForNode(@NonNull UiViewElementNode node) { + if (mNodeMap != null) { + return mNodeMap.get(node.getXmlNode()); + } + + return null; + } + + /** + * Returns a collection of nodes that have one or more lint warnings + * associated with them (retrievable via + * {@link #getIssueForNode(UiViewElementNode)}) + * + * @return a collection of nodes, which should <b>not</b> be modified by the + * caller + */ + @Nullable + public Collection<Node> getIssueNodes() { + if (mNodeMap != null) { + return mNodeMap.keySet(); + } + + return null; + } + + // ----- Extends LintClient ----- + + @Override + public void log(@NonNull Severity severity, @Nullable Throwable exception, + @Nullable String format, @Nullable Object... args) { + if (exception == null) { + AdtPlugin.log(IStatus.WARNING, format, args); + } else { + AdtPlugin.log(exception, format, args); + } + } + + @Override + public XmlParser getXmlParser() { + return new XmlParser() { + @Override + public Document parseXml(@NonNull XmlContext context) { + // Map File to IFile + IFile file = AdtUtils.fileToIFile(context.file); + if (file == null || !file.exists()) { + String path = context.file.getPath(); + AdtPlugin.log(IStatus.ERROR, "Can't find file %1$s in workspace", path); + return null; + } + + IStructuredModel model = null; + try { + IModelManager modelManager = StructuredModelManager.getModelManager(); + if (modelManager == null) { + // This can happen if incremental lint is running right as Eclipse is + // shutting down + return null; + } + model = modelManager.getModelForRead(file); + if (model instanceof IDOMModel) { + context.setProperty(MODEL_PROPERTY, model); + IDOMModel domModel = (IDOMModel) model; + return domModel.getDocument(); + } + } catch (IOException e) { + AdtPlugin.log(e, "Cannot read XML file"); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return null; + } + + @Override + public @NonNull Location getLocation(@NonNull XmlContext context, @NonNull Node node) { + IStructuredModel model = (IStructuredModel) context.getProperty(MODEL_PROPERTY); + return new LazyLocation(context.file, model.getStructuredDocument(), + (IndexedRegion) node); + } + + @Override + public @NonNull Location getLocation(@NonNull XmlContext context, @NonNull Node node, + int start, int end) { + IndexedRegion region = (IndexedRegion) node; + int nodeStart = region.getStartOffset(); + + IStructuredModel model = (IStructuredModel) context.getProperty(MODEL_PROPERTY); + // Get line number + LazyLocation location = new LazyLocation(context.file, + model.getStructuredDocument(), region); + int line = location.getStart().getLine(); + + Position startPos = new DefaultPosition(line, -1, nodeStart + start); + Position endPos = new DefaultPosition(line, -1, nodeStart + end); + return Location.create(context.file, startPos, endPos); + } + + @Override + public int getNodeStartOffset(@NonNull XmlContext context, @NonNull Node node) { + IndexedRegion region = (IndexedRegion) node; + return region.getStartOffset(); + } + + @Override + public int getNodeEndOffset(@NonNull XmlContext context, @NonNull Node node) { + IndexedRegion region = (IndexedRegion) node; + return region.getEndOffset(); + } + + @Override + public @NonNull Handle createLocationHandle(final @NonNull XmlContext context, + final @NonNull Node node) { + IStructuredModel model = (IStructuredModel) context.getProperty(MODEL_PROPERTY); + return new LazyLocation(context.file, model.getStructuredDocument(), + (IndexedRegion) node); + } + + @Override + public void dispose(@NonNull XmlContext context, @NonNull Document document) { + IStructuredModel model = (IStructuredModel) context.getProperty(MODEL_PROPERTY); + assert model != null : context.file; + if (model != null) { + model.releaseFromRead(); + } + } + + @Override + @NonNull + public Location getNameLocation(@NonNull XmlContext context, @NonNull Node node) { + return getLocation(context, node); + } + + @Override + @NonNull + public Location getValueLocation(@NonNull XmlContext context, @NonNull Attr node) { + return getLocation(context, node); + } + + }; + } + + @Override + public JavaParser getJavaParser(@Nullable Project project) { + if (mJavaParser == null) { + mJavaParser = new EclipseJavaParser(); + } + + return mJavaParser; + } + + // Cache for {@link getProject} + private IProject mLastEclipseProject; + private Project mLastLintProject; + + private IProject getProject(Project project) { + if (project == mLastLintProject) { + return mLastEclipseProject; + } + + mLastLintProject = project; + mLastEclipseProject = null; + + if (mResources != null) { + if (mResources.size() == 1) { + IProject p = mResources.get(0).getProject(); + mLastEclipseProject = p; + return p; + } + + IProject last = null; + for (IResource resource : mResources) { + IProject p = resource.getProject(); + if (p != last) { + if (project.getDir().equals(AdtUtils.getAbsolutePath(p).toFile())) { + mLastEclipseProject = p; + return p; + } + last = p; + } + } + } + + return null; + } + + @Override + @NonNull + public String getProjectName(@NonNull Project project) { + // Initialize the lint project's name to the name of the Eclipse project, + // which might differ from the directory name + IProject eclipseProject = getProject(project); + if (eclipseProject != null) { + return eclipseProject.getName(); + } + + return super.getProjectName(project); + } + + @NonNull + @Override + public Configuration getConfiguration(@NonNull Project project, @Nullable LintDriver driver) { + return getConfigurationFor(project); + } + + /** + * Same as {@link #getConfiguration(Project)}, but {@code project} can be + * null in which case the global configuration is returned. + * + * @param project the project to look up + * @return a corresponding configuration + */ + @NonNull + public Configuration getConfigurationFor(@Nullable Project project) { + if (project != null) { + IProject eclipseProject = getProject(project); + if (eclipseProject != null) { + return ProjectLintConfiguration.get(this, eclipseProject, mFatalOnly); + } + } + + return GlobalLintConfiguration.get(); + } + @Override + public void report(@NonNull Context context, @NonNull Issue issue, @NonNull Severity s, + @Nullable Location location, + @NonNull String message, @NonNull TextFormat format) { + message = format.toText(message); + int severity = getMarkerSeverity(s); + IMarker marker = null; + if (location != null) { + Position startPosition = location.getStart(); + if (startPosition == null) { + if (location.getFile() != null) { + IResource resource = AdtUtils.fileToResource(location.getFile()); + if (resource != null && resource.isAccessible()) { + marker = BaseProjectHelper.markResource(resource, MARKER_LINT, + message, 0, severity); + } + } + } else { + Position endPosition = location.getEnd(); + int line = startPosition.getLine() + 1; // Marker API is 1-based + IFile file = AdtUtils.fileToIFile(location.getFile()); + if (file != null && file.isAccessible()) { + Pair<Integer, Integer> r = getRange(file, mDocument, + startPosition, endPosition); + int startOffset = r.getFirst(); + int endOffset = r.getSecond(); + marker = BaseProjectHelper.markResource(file, MARKER_LINT, + message, line, startOffset, endOffset, severity); + } + } + } + + if (marker == null) { + marker = BaseProjectHelper.markResource(mResources.get(0), MARKER_LINT, + message, 0, severity); + } + + if (marker != null) { + // Store marker id such that we can recognize it from the suppress quickfix + try { + marker.setAttribute(MARKER_CHECKID_PROPERTY, issue.getId()); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + + if (s == Severity.FATAL) { + mWasFatal = true; + } + + if (mCollectNodes && location != null && marker != null) { + if (location instanceof LazyLocation) { + LazyLocation l = (LazyLocation) location; + IndexedRegion region = l.mRegion; + if (region instanceof Node) { + Node node = (Node) region; + if (node instanceof Attr) { + node = ((Attr) node).getOwnerElement(); + } + if (mNodeMap == null) { + mNodeMap = new WeakHashMap<Node, IMarker>(); + } + IMarker prev = mNodeMap.get(node); + if (prev != null) { + // Only replace the node if this node has higher priority + int prevSeverity = prev.getAttribute(IMarker.SEVERITY, 0); + if (prevSeverity < severity) { + mNodeMap.put(node, marker); + } + } else { + mNodeMap.put(node, marker); + } + } + } + } + } + + @Override + @Nullable + public File findResource(@NonNull String relativePath) { + // Look within the $ANDROID_SDK + String sdkFolder = AdtPrefs.getPrefs().getOsSdkFolder(); + if (sdkFolder != null) { + File file = new File(sdkFolder, relativePath); + if (file.exists()) { + return file; + } + } + + return null; + } + + /** + * Clears any lint markers from the given resource (project, folder or file) + * + * @param resource the resource to remove markers from + */ + public static void clearMarkers(@NonNull IResource resource) { + clearMarkers(Collections.singletonList(resource)); + } + + /** Clears any lint markers from the given list of resource (project, folder or file) */ + static void clearMarkers(List<? extends IResource> resources) { + for (IResource resource : resources) { + try { + if (resource.isAccessible()) { + resource.deleteMarkers(MARKER_LINT, false, IResource.DEPTH_INFINITE); + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + + IEditorPart activeEditor = AdtUtils.getActiveEditor(); + LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor); + if (delegate != null) { + delegate.getGraphicalEditor().getLayoutActionBar().updateErrorIndicator(); + } + } + + /** + * Removes all markers of the given id from the given resource. + * + * @param resource the resource to remove markers from (file or project, or + * null for all open projects) + * @param id the id for the issue whose markers should be deleted + */ + public static void removeMarkers(IResource resource, String id) { + if (resource == null) { + IJavaProject[] androidProjects = BaseProjectHelper.getAndroidProjects(null); + for (IJavaProject project : androidProjects) { + IProject p = project.getProject(); + if (p != null) { + // Recurse, but with a different parameter so it will not continue recursing + removeMarkers(p, id); + } + } + return; + } + IMarker[] markers = getMarkers(resource); + for (IMarker marker : markers) { + if (id.equals(getId(marker))) { + try { + marker.delete(); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + } + } + + /** + * Returns the lint marker for the given resource (which may be a project, folder or file) + * + * @param resource the resource to be checked, typically a source file + * @return an array of markers, possibly empty but never null + */ + public static IMarker[] getMarkers(IResource resource) { + try { + if (resource.isAccessible()) { + return resource.findMarkers(MARKER_LINT, false, IResource.DEPTH_INFINITE); + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return new IMarker[0]; + } + + private static int getMarkerSeverity(Severity severity) { + switch (severity) { + case INFORMATIONAL: + return IMarker.SEVERITY_INFO; + case WARNING: + return IMarker.SEVERITY_WARNING; + case FATAL: + case ERROR: + default: + return IMarker.SEVERITY_ERROR; + } + } + + private static Pair<Integer, Integer> getRange(IFile file, IDocument doc, + Position startPosition, Position endPosition) { + int startOffset = startPosition.getOffset(); + int endOffset = endPosition != null ? endPosition.getOffset() : -1; + if (endOffset != -1) { + // Attribute ranges often include trailing whitespace; trim this up + if (doc == null) { + IDocumentProvider provider = new TextFileDocumentProvider(); + try { + provider.connect(file); + doc = provider.getDocument(file); + if (doc != null) { + return adjustOffsets(doc, startOffset, endOffset); + } + } catch (Exception e) { + AdtPlugin.log(e, "Can't find range information for %1$s", file.getName()); + } finally { + provider.disconnect(file); + } + } else { + return adjustOffsets(doc, startOffset, endOffset); + } + } + + return Pair.of(startOffset, startOffset); + } + + /** + * Trim off any trailing space on the given offset range in the given + * document, and don't span multiple lines on ranges since it makes (for + * example) the XML editor just glow with yellow underlines for all the + * attributes etc. Highlighting just the element beginning gets the point + * across. It also makes it more obvious where there are warnings on both + * the overall element and on individual attributes since without this the + * warnings on attributes would just overlap with the whole-element + * highlighting. + */ + private static Pair<Integer, Integer> adjustOffsets(IDocument doc, int startOffset, + int endOffset) { + int originalStart = startOffset; + int originalEnd = endOffset; + + if (doc != null) { + while (endOffset > startOffset && endOffset < doc.getLength()) { + try { + if (!Character.isWhitespace(doc.getChar(endOffset - 1))) { + break; + } else { + endOffset--; + } + } catch (BadLocationException e) { + // Pass - we've already validated offset range above + break; + } + } + + // Also don't span lines + int lineEnd = startOffset; + while (lineEnd < endOffset) { + try { + char c = doc.getChar(lineEnd); + if (c == '\n' || c == '\r') { + endOffset = lineEnd; + if (endOffset > 0 && doc.getChar(endOffset - 1) == '\r') { + endOffset--; + } + break; + } + } catch (BadLocationException e) { + // Pass - we've already validated offset range above + break; + } + lineEnd++; + } + } + + if (startOffset >= endOffset) { + // Selecting nothing (for example, for the mangled CRLF delimiter issue selecting + // just the newline) + // In that case, use the real range + return Pair.of(originalStart, originalEnd); + } + + return Pair.of(startOffset, endOffset); + } + + /** + * Returns true if a fatal error was encountered + * + * @return true if a fatal error was encountered + */ + public boolean hasFatalErrors() { + return mWasFatal; + } + + /** + * Describe the issue for the given marker + * + * @param marker the marker to look up + * @return a full description of the corresponding issue, never null + */ + public static String describe(IMarker marker) { + IssueRegistry registry = getRegistry(); + String markerId = getId(marker); + Issue issue = registry.getIssue(markerId); + if (issue == null) { + return ""; + } + + String summary = issue.getBriefDescription(TextFormat.TEXT); + String explanation = issue.getExplanation(TextFormat.TEXT); + + StringBuilder sb = new StringBuilder(summary.length() + explanation.length() + 20); + try { + sb.append((String) marker.getAttribute(IMarker.MESSAGE)); + sb.append('\n').append('\n'); + } catch (CoreException e) { + } + sb.append("Issue: "); + sb.append(summary); + sb.append('\n'); + sb.append("Id: "); + sb.append(issue.getId()); + sb.append('\n').append('\n'); + sb.append(explanation); + + if (issue.getMoreInfo() != null) { + sb.append('\n').append('\n'); + sb.append(issue.getMoreInfo()); + } + + return sb.toString(); + } + + /** + * Returns the id for the given marker + * + * @param marker the marker to look up + * @return the corresponding issue id, or null + */ + public static String getId(IMarker marker) { + try { + return (String) marker.getAttribute(MARKER_CHECKID_PROPERTY); + } catch (CoreException e) { + return null; + } + } + + /** + * Shows the given marker in the editor + * + * @param marker the marker to be shown + */ + public static void showMarker(IMarker marker) { + IRegion region = null; + try { + int start = marker.getAttribute(IMarker.CHAR_START, -1); + int end = marker.getAttribute(IMarker.CHAR_END, -1); + if (start >= 0 && end >= 0) { + region = new org.eclipse.jface.text.Region(start, end - start); + } + + IResource resource = marker.getResource(); + if (resource instanceof IFile) { + IEditorPart editor = + AdtPlugin.openFile((IFile) resource, region, true /* showEditorTab */); + if (editor != null) { + IDE.gotoMarker(editor, marker); + } + } + } catch (PartInitException ex) { + AdtPlugin.log(ex, null); + } + } + + /** + * Show a dialog with errors for the given file + * + * @param shell the parent shell to attach the dialog to + * @param file the file to show the errors for + * @param editor the editor for the file, if known + */ + public static void showErrors( + @NonNull Shell shell, + @NonNull IFile file, + @Nullable IEditorPart editor) { + LintListDialog dialog = new LintListDialog(shell, file, editor); + dialog.open(); + } + + @Override + public @NonNull String readFile(@NonNull File f) { + // Map File to IFile + IFile file = AdtUtils.fileToIFile(f); + if (file == null || !file.exists()) { + String path = f.getPath(); + AdtPlugin.log(IStatus.ERROR, "Can't find file %1$s in workspace", path); + return readPlainFile(f); + } + + if (SdkUtils.endsWithIgnoreCase(file.getName(), DOT_XML)) { + IStructuredModel model = null; + try { + IModelManager modelManager = StructuredModelManager.getModelManager(); + model = modelManager.getModelForRead(file); + return model.getStructuredDocument().get(); + } catch (IOException e) { + AdtPlugin.log(e, "Cannot read XML file"); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } finally { + if (model != null) { + // TODO: This may be too early... + model.releaseFromRead(); + } + } + } + + return readPlainFile(f); + } + + private String readPlainFile(File file) { + try { + return LintUtils.getEncodedString(this, file); + } catch (IOException e) { + return ""; //$NON-NLS-1$ + } + } + + private Map<Project, ClassPathInfo> mProjectInfo; + + @Override + @NonNull + protected ClassPathInfo getClassPath(@NonNull Project project) { + ClassPathInfo info; + if (mProjectInfo == null) { + mProjectInfo = Maps.newHashMap(); + info = null; + } else { + info = mProjectInfo.get(project); + } + + if (info == null) { + List<File> sources = null; + List<File> classes = null; + List<File> libraries = null; + + IProject p = getProject(project); + if (p != null) { + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(p); + + // Output path + File file = workspacePathToFile(javaProject.getOutputLocation()); + classes = Collections.singletonList(file); + + // Source path + IClasspathEntry[] entries = javaProject.getRawClasspath(); + sources = new ArrayList<File>(entries.length); + libraries = new ArrayList<File>(entries.length); + for (int i = 0; i < entries.length; i++) { + IClasspathEntry entry = entries[i]; + int kind = entry.getEntryKind(); + + if (kind == IClasspathEntry.CPE_VARIABLE) { + entry = JavaCore.getResolvedClasspathEntry(entry); + if (entry == null) { + // It's possible that the variable is no longer valid; ignore + continue; + } + kind = entry.getEntryKind(); + } + + if (kind == IClasspathEntry.CPE_SOURCE) { + sources.add(workspacePathToFile(entry.getPath())); + } else if (kind == IClasspathEntry.CPE_LIBRARY) { + libraries.add(entry.getPath().toFile()); + } + // Note that we ignore IClasspathEntry.CPE_CONTAINER: + // Normal Android Eclipse projects supply both + // AdtConstants.CONTAINER_FRAMEWORK + // and + // AdtConstants.CONTAINER_LIBRARIES + // here. We ignore the framework classes for obvious reasons, + // but we also ignore the library container because lint will + // process the libraries differently. When Eclipse builds a + // project, it gets the .jar output of the library projects + // from this container, which means it doesn't have to process + // the library sources. Lint on the other hand wants to process + // the source code, so instead it actually looks at the + // project.properties file to find the libraries, and then it + // iterates over all the library projects in turn and analyzes + // those separately (but passing the main project for context, + // such that the including project's manifest declarations + // are used for data like minSdkVersion level). + // + // Note that this container will also contain *other* + // libraries (Java libraries, not library projects) that we + // *should* include. However, we can't distinguish these + // class path entries from the library project jars, + // so instead of looking at these, we simply listFiles() in + // the libs/ folder after processing the classpath info + } + + // Add in libraries + File libs = new File(project.getDir(), FD_NATIVE_LIBS); + if (libs.isDirectory()) { + File[] jars = libs.listFiles(); + if (jars != null) { + for (File jar : jars) { + if (SdkUtils.endsWith(jar.getPath(), DOT_JAR)) { + libraries.add(jar); + } + } + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + + if (sources == null) { + sources = super.getClassPath(project).getSourceFolders(); + } + if (classes == null) { + classes = super.getClassPath(project).getClassFolders(); + } + if (libraries == null) { + libraries = super.getClassPath(project).getLibraries(); + } + + + // No test folders in Eclipse: + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=224708 + List<File> tests = Collections.emptyList(); + + info = new ClassPathInfo(sources, classes, libraries, tests); + mProjectInfo.put(project, info); + } + + return info; + } + + /** + * Returns the registry of issues to check from within Eclipse. + * + * @return the issue registry to use to access detectors and issues + */ + public static IssueRegistry getRegistry() { + return new EclipseLintIssueRegistry(); + } + + @Override + public @NonNull Class<? extends Detector> replaceDetector( + @NonNull Class<? extends Detector> detectorClass) { + return detectorClass; + } + + @Override + @NonNull + public IAndroidTarget[] getTargets() { + Sdk sdk = Sdk.getCurrent(); + if (sdk != null) { + return sdk.getTargets(); + } else { + return new IAndroidTarget[0]; + } + } + + private boolean mSearchForSuperClasses; + + /** + * Sets whether this client should search for super types on its own. This + * is typically not needed when doing a full lint run (because lint will + * look at all classes and libraries), but is useful during incremental + * analysis when lint is only looking at a subset of classes. In that case, + * we want to use Eclipse's data structures for super classes. + * + * @param search whether to use a custom Eclipse search for super class + * names + */ + public void setSearchForSuperClasses(boolean search) { + mSearchForSuperClasses = search; + } + + /** + * Whether this lint client is searching for super types. See + * {@link #setSearchForSuperClasses(boolean)} for details. + * + * @return whether the client will search for super types + */ + public boolean getSearchForSuperClasses() { + return mSearchForSuperClasses; + } + + @Override + @Nullable + public String getSuperClass(@NonNull Project project, @NonNull String name) { + if (!mSearchForSuperClasses) { + // Super type search using the Eclipse index is potentially slow, so + // only do this when necessary + return null; + } + + IProject eclipseProject = getProject(project); + if (eclipseProject == null) { + return null; + } + + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(eclipseProject); + if (javaProject == null) { + return null; + } + + String typeFqcn = ClassContext.getFqcn(name); + IType type = javaProject.findType(typeFqcn); + if (type != null) { + ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor()); + IType superType = hierarchy.getSuperclass(type); + if (superType != null) { + String key = superType.getKey(); + if (!key.isEmpty() + && key.charAt(0) == 'L' + && key.charAt(key.length() - 1) == ';') { + return key.substring(1, key.length() - 1); + } else { + String fqcn = superType.getFullyQualifiedName(); + return ClassContext.getInternalName(fqcn); + } + } + } + } catch (JavaModelException e) { + log(Severity.INFORMATIONAL, e, null); + } catch (CoreException e) { + log(Severity.INFORMATIONAL, e, null); + } + + return null; + } + + @Override + @Nullable + public Boolean isSubclassOf( + @NonNull Project project, + @NonNull String name, @NonNull + String superClassName) { + if (!mSearchForSuperClasses) { + // Super type search using the Eclipse index is potentially slow, so + // only do this when necessary + return null; + } + + IProject eclipseProject = getProject(project); + if (eclipseProject == null) { + return null; + } + + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(eclipseProject); + if (javaProject == null) { + return null; + } + + String typeFqcn = ClassContext.getFqcn(name); + IType type = javaProject.findType(typeFqcn); + if (type != null) { + ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor()); + IType[] allSupertypes = hierarchy.getAllSuperclasses(type); + if (allSupertypes != null) { + String target = 'L' + superClassName + ';'; + for (IType superType : allSupertypes) { + if (target.equals(superType.getKey())) { + return Boolean.TRUE; + } + } + return Boolean.FALSE; + } + } + } catch (JavaModelException e) { + log(Severity.INFORMATIONAL, e, null); + } catch (CoreException e) { + log(Severity.INFORMATIONAL, e, null); + } + + return null; + } + + private static class LazyLocation extends Location implements Location.Handle { + private final IStructuredDocument mDocument; + private final IndexedRegion mRegion; + private Position mStart; + private Position mEnd; + + public LazyLocation(File file, IStructuredDocument document, IndexedRegion region) { + super(file, null /*start*/, null /*end*/); + mDocument = document; + mRegion = region; + } + + @Override + public Position getStart() { + if (mStart == null) { + int line = -1; + int column = -1; + int offset = mRegion.getStartOffset(); + + if (mRegion instanceof org.w3c.dom.Text && mDocument != null) { + // For text nodes, skip whitespace prefix, if any + for (int i = offset; + i < mRegion.getEndOffset() && i < mDocument.getLength(); i++) { + try { + char c = mDocument.getChar(i); + if (!Character.isWhitespace(c)) { + offset = i; + break; + } + } catch (BadLocationException e) { + break; + } + } + } + + if (mDocument != null && offset < mDocument.getLength()) { + line = mDocument.getLineOfOffset(offset); + column = -1; + try { + int lineOffset = mDocument.getLineOffset(line); + column = offset - lineOffset; + } catch (BadLocationException e) { + AdtPlugin.log(e, null); + } + } + + mStart = new DefaultPosition(line, column, offset); + } + + return mStart; + } + + @Override + public Position getEnd() { + if (mEnd == null) { + mEnd = new DefaultPosition(-1, -1, mRegion.getEndOffset()); + } + + return mEnd; + } + + @Override + public @NonNull Location resolve() { + return this; + } + } + + private static class EclipseJavaParser extends JavaParser { + private static final boolean USE_ECLIPSE_PARSER = true; + private final Parser mParser; + + EclipseJavaParser() { + if (USE_ECLIPSE_PARSER) { + CompilerOptions options = new CompilerOptions(); + // Always using JDK 7 rather than basing it on project metadata since we + // don't do compilation error validation in lint (we leave that to the IDE's + // error parser or the command line build's compilation step); we want an + // AST that is as tolerant as possible. + options.complianceLevel = ClassFileConstants.JDK1_7; + options.sourceLevel = ClassFileConstants.JDK1_7; + options.targetJDK = ClassFileConstants.JDK1_7; + options.parseLiteralExpressionsAsConstants = true; + ProblemReporter problemReporter = new ProblemReporter( + DefaultErrorHandlingPolicies.exitOnFirstError(), + options, + new DefaultProblemFactory()); + mParser = new Parser(problemReporter, options.parseLiteralExpressionsAsConstants); + mParser.javadocParser.checkDocComment = false; + } else { + mParser = null; + } + } + + @Override + public void prepareJavaParse(@NonNull List<JavaContext> contexts) { + // TODO: Use batch compiler from lint-cli.jar + } + + @Override + public lombok.ast.Node parseJava(@NonNull JavaContext context) { + if (USE_ECLIPSE_PARSER) { + // Use Eclipse's compiler + EcjTreeConverter converter = new EcjTreeConverter(); + String code = context.getContents(); + + CompilationUnit sourceUnit = new CompilationUnit(code.toCharArray(), + context.file.getName(), "UTF-8"); //$NON-NLS-1$ + CompilationResult compilationResult = new CompilationResult(sourceUnit, 0, 0, 0); + CompilationUnitDeclaration unit = null; + try { + unit = mParser.parse(sourceUnit, compilationResult); + } catch (AbortCompilation e) { + // No need to report Java parsing errors while running in Eclipse. + // Eclipse itself will already provide problem markers for these files, + // so all this achieves is creating "multiple annotations on this line" + // tooltips instead. + return null; + } + if (unit == null) { + return null; + } + + try { + converter.visit(code, unit); + List<? extends lombok.ast.Node> nodes = converter.getAll(); + + // There could be more than one node when there are errors; pick out the + // compilation unit node + for (lombok.ast.Node node : nodes) { + if (node instanceof lombok.ast.CompilationUnit) { + return node; + } + } + + return null; + } catch (Throwable t) { + AdtPlugin.log(t, "Failed converting ECJ parse tree to Lombok for file %1$s", + context.file.getPath()); + return null; + } + } else { + // Use Lombok for now + Source source = new Source(context.getContents(), context.file.getName()); + List<lombok.ast.Node> nodes = source.getNodes(); + + // Don't analyze files containing errors + List<ParseProblem> problems = source.getProblems(); + if (problems != null && problems.size() > 0) { + /* Silently ignore the errors. There are still some bugs in Lombok/Parboiled + * (triggered if you run lint on the AOSP framework directory for example), + * and having these show up as fatal errors when it's really a tool bug + * is bad. To make matters worse, the error messages aren't clear: + * http://code.google.com/p/projectlombok/issues/detail?id=313 + for (ParseProblem problem : problems) { + lombok.ast.Position position = problem.getPosition(); + Location location = Location.create(context.file, + context.getContents(), position.getStart(), position.getEnd()); + String message = problem.getMessage(); + context.report( + IssueRegistry.PARSER_ERROR, location, + message, + null); + + } + */ + return null; + } + + // There could be more than one node when there are errors; pick out the + // compilation unit node + for (lombok.ast.Node node : nodes) { + if (node instanceof lombok.ast.CompilationUnit) { + return node; + } + } + return null; + } + } + + @Override + public @NonNull Location getLocation(@NonNull JavaContext context, + @NonNull lombok.ast.Node node) { + lombok.ast.Position position = node.getPosition(); + return Location.create(context.file, context.getContents(), + position.getStart(), position.getEnd()); + } + + @Override + public @NonNull Handle createLocationHandle(@NonNull JavaContext context, + @NonNull lombok.ast.Node node) { + return new LocationHandle(context.file, node); + } + + @Override + public void dispose(@NonNull JavaContext context, + @NonNull lombok.ast.Node compilationUnit) { + } + + @Override + @Nullable + public ResolvedNode resolve(@NonNull JavaContext context, + @NonNull lombok.ast.Node node) { + return null; + } + + @Override + @Nullable + public TypeDescriptor getType(@NonNull JavaContext context, + @NonNull lombok.ast.Node node) { + return null; + } + + /* Handle for creating positions cheaply and returning full fledged locations later */ + private class LocationHandle implements Handle { + private File mFile; + private lombok.ast.Node mNode; + private Object mClientData; + + public LocationHandle(File file, lombok.ast.Node node) { + mFile = file; + mNode = node; + } + + @Override + public @NonNull Location resolve() { + lombok.ast.Position pos = mNode.getPosition(); + return Location.create(mFile, null /*contents*/, pos.getStart(), pos.getEnd()); + } + + @Override + public void setClientData(@Nullable Object clientData) { + mClientData = clientData; + } + + @Override + @Nullable + public Object getClientData() { + return mClientData; + } + } + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintIssueRegistry.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintIssueRegistry.java new file mode 100644 index 000000000..3abbdeb84 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintIssueRegistry.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import com.android.annotations.NonNull; +import com.android.tools.lint.checks.*; +import com.android.tools.lint.detector.api.Issue; + +import java.util.ArrayList; +import java.util.List; + +public class EclipseLintIssueRegistry extends BuiltinIssueRegistry { + private static List<Issue> sFilteredIssues; + + public EclipseLintIssueRegistry() { + } + + @NonNull + @Override + public List<Issue> getIssues() { + if (sFilteredIssues == null) { + // Remove issues that do not work properly in Eclipse + List<Issue> sIssues = super.getIssues(); + List<Issue> result = new ArrayList<Issue>(sIssues.size()); + for (Issue issue : sIssues) { + if (issue == MissingClassDetector.INSTANTIATABLE) { + // Apparently violated by + // android.support.v7.internal.widget.ActionBarView.HomeView + // See issue 72760 + continue; + } else if (issue == DuplicateIdDetector.WITHIN_LAYOUT) { + // Apparently violated by + // sdk/extras/android/support/v7/appcompat/abc_activity_chooser_view_include.xml + // See issue 72760 + continue; + } else if (issue == AppCompatResourceDetector.ISSUE + || issue == AppCompatCallDetector.ISSUE) { + // Apparently has some false positives in Eclipse; see issue + // 72824 + continue; + } else if (issue.getImplementation().getDetectorClass() == RtlDetector.class) { + // False positives in Eclipse; see issue 78780 + continue; + } + result.add(issue); + } + sFilteredIssues = result; + } + + return sFilteredIssues; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintRunner.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintRunner.java new file mode 100644 index 000000000..43cd48d1b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintRunner.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.tools.lint.client.api.IssueRegistry; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.text.IDocument; +import org.eclipse.swt.widgets.Shell; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +/** + * Eclipse implementation for running lint on workspace files and projects. + */ +public class EclipseLintRunner { + static final String MARKER_CHECKID_PROPERTY = "checkid"; //$NON-NLS-1$ + + /** + * Runs lint and updates the markers, and waits for the result. Returns + * true if fatal errors were found. + * + * @param resources the resources (project, folder or file) to be analyzed + * @param source if checking a single source file, the source file + * @param doc the associated document, if known, or null + * @param fatalOnly if true, only report fatal issues (severity=error) + * @return true if any fatal errors were encountered. + */ + private static boolean runLint( + @NonNull List<? extends IResource> resources, + @Nullable IResource source, + @Nullable IDocument doc, + boolean fatalOnly) { + resources = addLibraries(resources); + LintJob job = (LintJob) startLint(resources, source, doc, fatalOnly, + false /*show*/); + try { + job.join(); + boolean fatal = job.isFatal(); + + if (fatal) { + LintViewPart.show(resources); + } + + return fatal; + } catch (InterruptedException e) { + AdtPlugin.log(e, null); + } + + return false; + } + + /** + * Runs lint and updates the markers. Does not wait for the job to finish - + * just returns immediately. + * + * @param resources the resources (project, folder or file) to be analyzed + * @param source if checking a single source file, the source file. When + * single checking an XML file, this is typically the same as the + * file passed in the list in the first parameter, but when + * checking the .class files of a Java file for example, the + * .class file and all the inner classes of the Java file are + * passed in the first parameter, and the corresponding .java + * source file is passed here. + * @param doc the associated document, if known, or null + * @param fatalOnly if true, only report fatal issues (severity=error) + * @param show if true, show the results in a {@link LintViewPart} + * @return the job running lint in the background. + */ + public static Job startLint( + @NonNull List<? extends IResource> resources, + @Nullable IResource source, + @Nullable IDocument doc, + boolean fatalOnly, + boolean show) { + IssueRegistry registry = EclipseLintClient.getRegistry(); + EclipseLintClient client = new EclipseLintClient(registry, resources, doc, fatalOnly); + return startLint(client, resources, source, show); + } + + /** + * Runs lint and updates the markers. Does not wait for the job to finish - + * just returns immediately. + * + * @param client the lint client receiving issue reports etc + * @param resources the resources (project, folder or file) to be analyzed + * @param source if checking a single source file, the source file. When + * single checking an XML file, this is typically the same as the + * file passed in the list in the first parameter, but when + * checking the .class files of a Java file for example, the + * .class file and all the inner classes of the Java file are + * passed in the first parameter, and the corresponding .java + * source file is passed here. + * @param show if true, show the results in a {@link LintViewPart} + * @return the job running lint in the background. + */ + public static Job startLint( + @NonNull EclipseLintClient client, + @NonNull List<? extends IResource> resources, + @Nullable IResource source, + boolean show) { + if (resources != null && !resources.isEmpty()) { + if (!AdtPrefs.getPrefs().getSkipLibrariesFromLint()) { + resources = addLibraries(resources); + } + + cancelCurrentJobs(false); + + LintJob job = new LintJob(client, resources, source); + job.schedule(); + + if (show) { + // Show lint view where the results are listed + LintViewPart.show(resources); + } + return job; + } + + return null; + } + + /** + * Run Lint for an Export APK action. If it succeeds (no fatal errors) + * returns true, and if it fails it will display an error message and return + * false. + * + * @param shell the parent shell to show error messages in + * @param project the project to run lint on + * @return true if the lint run succeeded with no fatal errors + */ + public static boolean runLintOnExport(Shell shell, IProject project) { + if (AdtPrefs.getPrefs().isLintOnExport()) { + boolean fatal = EclipseLintRunner.runLint(Collections.singletonList(project), + null, null, true /*fatalOnly*/); + if (fatal) { + MessageDialog.openWarning(shell, + "Export Aborted", + "Export aborted because fatal lint errors were found. These " + + "are listed in the Lint View. Either fix these before " + + "running Export again, or turn off \"Run full error check " + + "when exporting app\" in the Android > Lint Error Checking " + + "preference page."); + return false; + } + } + + return true; + } + + /** Cancels the current lint jobs, if any, and optionally waits for them to finish */ + static void cancelCurrentJobs(boolean wait) { + // Cancel any current running jobs first + Job[] currentJobs = LintJob.getCurrentJobs(); + for (Job job : currentJobs) { + job.cancel(); + } + + if (wait) { + for (Job job : currentJobs) { + try { + job.join(); + } catch (InterruptedException e) { + AdtPlugin.log(e, null); + } + } + } + } + + /** If the resource list contains projects, add in any library projects as well */ + private static List<? extends IResource> addLibraries(List<? extends IResource> resources) { + if (resources != null && !resources.isEmpty()) { + boolean haveProjects = false; + for (IResource resource : resources) { + if (resource instanceof IProject) { + haveProjects = true; + break; + } + } + + if (haveProjects) { + List<IResource> result = new ArrayList<IResource>(); + Map<IProject, IProject> allProjects = new IdentityHashMap<IProject, IProject>(); + List<IProject> projects = new ArrayList<IProject>(); + for (IResource resource : resources) { + if (resource instanceof IProject) { + IProject project = (IProject) resource; + allProjects.put(project, project); + projects.add(project); + } else { + result.add(resource); + } + } + for (IProject project : projects) { + ProjectState state = Sdk.getProjectState(project); + if (state != null) { + for (IProject library : state.getFullLibraryProjects()) { + allProjects.put(library, library); + } + } + } + for (IProject project : allProjects.keySet()) { + result.add(project); + } + + return result; + } + } + + return resources; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ExtractStringFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ExtractStringFix.java new file mode 100644 index 000000000..7eafd4364 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ExtractStringFix.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringRefactoring; +import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringWizard; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextSelection; +import org.eclipse.jface.text.TextSelection; +import org.eclipse.ltk.ui.refactoring.RefactoringWizard; +import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.w3c.dom.Node; + +/** + * Fix for extracting strings. + * <p> + * TODO: Look for existing string values, and if it matches one of the + * existing Strings offer to just replace it with the given string! + */ +@SuppressWarnings("restriction") // DOM model +final class ExtractStringFix extends DocumentFix { + private ExtractStringFix(String id, IMarker marker) { + super(id, marker); + } + + @Override + public boolean needsFocus() { + return true; + } + + @Override + public boolean isCancelable() { + return true; + } + + @Override + protected void apply(IDocument document, IStructuredModel model, Node node, int start, + int end) { + IEditorPart editorPart = AdtUtils.getActiveEditor(); + if (editorPart instanceof AndroidXmlEditor) { + IFile file = (IFile) mMarker.getResource(); + ITextSelection selection = new TextSelection(start, end - start); + + ExtractStringRefactoring refactoring = + new ExtractStringRefactoring(file, editorPart, selection); + RefactoringWizard wizard = new ExtractStringWizard(refactoring, file.getProject()); + RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard); + try { + IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + op.run(window.getShell(), wizard.getDefaultPageTitle()); + } catch (InterruptedException e) { + } + } + } + + @Override + public String getDisplayString() { + return "Extract String"; + } + + @Override + public Image getImage() { + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + return sharedImages.getImage(ISharedImages.IMG_OBJ_ADD); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/GlobalLintConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/GlobalLintConfiguration.java new file mode 100644 index 000000000..2f0261e7b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/GlobalLintConfiguration.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.tools.lint.client.api.Configuration; +import com.android.tools.lint.client.api.IssueRegistry; +import com.android.tools.lint.detector.api.Context; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.Location; +import com.android.tools.lint.detector.api.Severity; + +import org.eclipse.jface.preference.IPreferenceStore; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Global (non-project-specific) configuration for Lint in Eclipse */ +class GlobalLintConfiguration extends Configuration { + private static final GlobalLintConfiguration sInstance = new GlobalLintConfiguration(); + + private Map<Issue, Severity> mSeverities; + private boolean mBulkEditing; + + private GlobalLintConfiguration() { + } + + /** + * Obtain a reference to the singleton + * + * @return the singleton configuration + */ + @NonNull + public static GlobalLintConfiguration get() { + return sInstance; + } + + @Override + public Severity getSeverity(@NonNull Issue issue) { + if (mSeverities == null) { + IssueRegistry registry = EclipseLintClient.getRegistry(); + mSeverities = new HashMap<Issue, Severity>(); + IPreferenceStore store = getStore(); + String assignments = store.getString(AdtPrefs.PREFS_LINT_SEVERITIES); + if (assignments != null && assignments.length() > 0) { + for (String assignment : assignments.split(",")) { //$NON-NLS-1$ + String[] s = assignment.split("="); //$NON-NLS-1$ + if (s.length == 2) { + Issue d = registry.getIssue(s[0]); + if (d != null) { + Severity severity = Severity.valueOf(s[1]); + if (severity != null) { + mSeverities.put(d, severity); + } + } + } + } + } + } + + Severity severity = mSeverities.get(issue); + if (severity != null) { + return severity; + } + + if (!issue.isEnabledByDefault()) { + return Severity.IGNORE; + } + + return issue.getDefaultSeverity(); + } + + private IPreferenceStore getStore() { + IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); + return store; + } + + @Override + public void ignore(@NonNull Context context, @NonNull Issue issue, + @Nullable Location location, @NonNull String message) { + throw new UnsupportedOperationException( + "Can't ignore() in global configurations"); //$NON-NLS-1$ + } + + @Override + public void setSeverity(@NonNull Issue issue, @Nullable Severity severity) { + if (mSeverities == null) { + // Force initialization + getSeverity(issue); + } + + if (severity == null) { + mSeverities.remove(issue); + } else { + mSeverities.put(issue, severity); + } + + if (!mBulkEditing) { + setSeverities(mSeverities); + } + } + + /** + * Sets the custom severities for the given issues, in bulk. + * + * @param severities a map from detector to severity to use from now on + * @return true if something changed from the current settings + */ + private boolean setSeverities(Map<Issue, Severity> severities) { + mSeverities = severities; + + String value = ""; + if (severities.size() > 0) { + List<Issue> sortedKeys = new ArrayList<Issue>(severities.keySet()); + Collections.sort(sortedKeys); + + StringBuilder sb = new StringBuilder(severities.size() * 20); + for (Issue issue : sortedKeys) { + Severity severity = severities.get(issue); + if (severity != issue.getDefaultSeverity()) { + if (sb.length() > 0) { + sb.append(','); + } + sb.append(issue.getId()); + sb.append('='); + sb.append(severity.name()); + } + } + + value = sb.toString(); + } + + IPreferenceStore store = getStore(); + String previous = store.getString(AdtPrefs.PREFS_LINT_SEVERITIES); + boolean changed = !value.equals(previous); + if (changed) { + if (value.length() == 0) { + store.setToDefault(AdtPrefs.PREFS_LINT_SEVERITIES); + } else { + store.setValue(AdtPrefs.PREFS_LINT_SEVERITIES, value); + } + } + + return changed; + } + + @Override + public void startBulkEditing() { + mBulkEditing = true; + } + + @Override + public void finishBulkEditing() { + mBulkEditing = false; + setSeverities(mSeverities); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/InputDensityDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/InputDensityDialog.java new file mode 100644 index 000000000..d102225b2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/InputDensityDialog.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import com.android.resources.Density; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +import java.util.ArrayList; +import java.util.List; + +class InputDensityDialog extends Dialog { + private Combo mCombo; + /** + * Density value being chosen - static to keep most recently chosen value + * across repeated invocations + */ + private static int sDpi = Density.DEFAULT_DENSITY; + + InputDensityDialog(Shell parentShell) { + super(parentShell); + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite container = (Composite) super.createDialogArea(parent); + container.setLayout(new GridLayout(1, false)); + + Label lblWhatIsThe = new Label(container, SWT.WRAP); + lblWhatIsThe.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1)); + lblWhatIsThe.setText("What is the screen density the current px value works with?"); + + mCombo = new Combo(container, SWT.READ_ONLY); + GridData gdCombo = new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1); + gdCombo.widthHint = 200; + mCombo.setLayoutData(gdCombo); + int initialIndex = 0; + List<String> s = new ArrayList<String>(); + int index = 0; + for (Density density : Density.values()) { + if (density == Density.NODPI) { + continue; + } + if (density.getDpiValue() == sDpi) { + initialIndex = index; + } + s.add(getLabel(density)); + index++; + } + String[] items = s.toArray(new String[s.size()]); + mCombo.setItems(items); + mCombo.select(initialIndex); + + return container; + } + + private static String getLabel(Density density) { + return String.format("%1$s (%2$d)", density.getShortDisplayValue(), density.getDpiValue()); + } + + @Override + protected void createButtonsForButtonBar(Composite parent) { + createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); + createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false); + } + + @Override + protected Point getInitialSize() { + return new Point(450, 150); + } + + @Override + public boolean close() { + String description = mCombo.getItem(mCombo.getSelectionIndex()); + + for (Density density : Density.values()) { + if (description.equals(getLabel(density))) { + sDpi = density.getDpiValue(); + break; + } + } + + return super.close(); + } + + @Override + protected void configureShell(Shell shell) { + super.configureShell(shell); + shell.setText("Choose Density"); + } + + int getDensity() { + return sDpi; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LinearLayoutWeightFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LinearLayoutWeightFix.java new file mode 100644 index 000000000..af3fc74eb --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LinearLayoutWeightFix.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; +import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; +import static com.android.SdkConstants.ATTR_ORIENTATION; +import static com.android.SdkConstants.VALUE_VERTICAL; +import static com.android.SdkConstants.VALUE_ZERO_DP; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.text.IDocument; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +@SuppressWarnings("restriction") // DOM model +final class LinearLayoutWeightFix extends DocumentFix { + private LinearLayoutWeightFix(String id, IMarker marker) { + super(id, marker); + } + + @Override + public boolean needsFocus() { + return false; + } + + @Override + public boolean isCancelable() { + return false; + } + + @Override + protected void apply(IDocument document, IStructuredModel model, Node node, int start, + int end) { + if (node instanceof Element && node.getParentNode() instanceof Element) { + Element element = (Element) node; + Element parent = (Element) node.getParentNode(); + String dimension; + if (VALUE_VERTICAL.equals(parent.getAttributeNS(ANDROID_URI, + ATTR_ORIENTATION))) { + dimension = ATTR_LAYOUT_HEIGHT; + } else { + dimension = ATTR_LAYOUT_WIDTH; + } + element.setAttributeNS(ANDROID_URI, dimension, VALUE_ZERO_DP); + } + } + + @Override + public String getDisplayString() { + return "Replace size attribute with 0dp"; + } + + @Override + public Image getImage() { + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + // TODO: Need a better icon here + return sharedImages.getImage(ISharedImages.IMG_OBJ_ELEMENT); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintColumn.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintColumn.java new file mode 100644 index 000000000..297d94b79 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintColumn.java @@ -0,0 +1,532 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils; +import com.android.tools.lint.detector.api.Issue; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.viewers.StyledString; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; + +import java.io.File; +import java.util.Comparator; + +/** A column shown in the {@link LintList} */ +abstract class LintColumn implements Comparator<IMarker> { + protected final LintList mList; + + protected LintColumn(@NonNull LintList list) { + mList = list; + } + + /** @return true if this column should be shown by default */ + public boolean isVisibleByDefault() { + return true; + } + + /** @return true if this column's text should be left aligned */ + public boolean isLeftAligned() { + return true; + } + + /** + * @return the number of pixels that this column should show by default + */ + public int getPreferredWidth() { + return getPreferredCharWidth() * SwtUtils.getAverageCharWidth(mList.getDisplay(), + mList.getTree().getFont()); + } + + /** + * @return the number of characters that this column should show by default + */ + public int getPreferredCharWidth() { + return 15; + } + + /** + * @return the title of the column + */ + @NonNull + public abstract String getColumnHeaderText(); + + /** + * @return the image of the column, or null + */ + public Image getColumnHeaderImage() { + return null; + } + + /** + * @param marker the {@link IMarker} to get the value for + * @return the value of this column for the given marker + */ + public abstract String getValue(@NonNull IMarker marker); + + /** + * @param marker the {@link IMarker} to get the value for + * @return the styled value of this column for the given marker + */ + public StyledString getStyledValue(@NonNull IMarker marker) { + return null; + } + + /** + * @param marker the {@link IMarker} to get the image for + * @return The image for this particular column, or null + */ + @Nullable + public Image getImage(@NonNull IMarker marker) { + return null; + } + + /** + * @param marker the {@link IMarker} to get the font for + * @return The font for this particular column, or null + */ + @Nullable + public Font getFont(@NonNull IMarker marker) { + return null; + } + + /** + * @return true if the sort should be in ascending order. If false, sort in descending order. + */ + public boolean isAscending() { + return true; + } + + /** + * @return true if this column should be visible by default + */ + public boolean visibleByDefault() { + return true; + } + + @Override + public int compare(IMarker o1, IMarker o2) { + return getValue(o1).compareTo(getValue(o2)); + } + + // Used for default LabelProvider + @Override + public String toString() { + return getColumnHeaderText(); + } + + static class MessageColumn extends LintColumn { + + public MessageColumn(LintList list) { + super(list); + } + + @Override + public @NonNull String getColumnHeaderText() { + return "Description"; + } + + @Override + public int getPreferredCharWidth() { + return 80; + } + + @Override + public String getValue(@NonNull IMarker marker) { + return getStyledValue(marker).toString(); + } + + @Override + public StyledString getStyledValue(@NonNull IMarker marker) { + StyledString styledString = new StyledString(); + + String message = marker.getAttribute(IMarker.MESSAGE, ""); + styledString.append(message); + + int count = mList.getCount(marker); + if (count > 1) { + styledString.append(String.format(" (%2$d items)", message, count), + StyledString.COUNTER_STYLER); + } + + return styledString; + } + + @Override + public Image getImage(@NonNull IMarker marker) { + int severity = marker.getAttribute(IMarker.SEVERITY, 0); + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + switch (severity) { + case IMarker.SEVERITY_ERROR: + if (LintFix.hasFix(EclipseLintClient.getId(marker))) { + return IconFactory.getInstance().getIcon("quickfix_error"); //$NON-NLS-1$ + } + return sharedImages.getImage(ISharedImages.IMG_OBJS_ERROR_TSK); + case IMarker.SEVERITY_WARNING: + if (LintFix.hasFix(EclipseLintClient.getId(marker))) { + return IconFactory.getInstance().getIcon("quickfix_warning"); //$NON-NLS-1$ + } + return sharedImages.getImage(ISharedImages.IMG_OBJS_WARN_TSK); + case IMarker.SEVERITY_INFO: + return sharedImages.getImage(ISharedImages.IMG_OBJS_INFO_TSK); + default: + return null; + } + } + + @Override + public Font getFont(@NonNull IMarker marker) { + int severity = marker.getAttribute(IMarker.SEVERITY, 0); + if (severity == IMarker.SEVERITY_ERROR) { + return JFaceResources.getFontRegistry().getBold( + JFaceResources.DEFAULT_FONT); + } + + return null; + } + + @Override + public boolean isAscending() { + return false; + } + + @Override + public int compare(IMarker marker2, IMarker marker1) { + // Reversing order marker1/marker2 here since we want to use a "descending" column + // sorting marker to indicate priority. (Note that we return from isAscending too.) + + String id1 = EclipseLintClient.getId(marker1); + String id2 = EclipseLintClient.getId(marker2); + if (id1 == null || id2 == null) { + return marker1.getResource().getName().compareTo( + marker2.getResource().getName()); + } + Issue issue1 = mList.getIssue(id1); + Issue issue2 = mList.getIssue(id2); + if (issue1 == null || issue2 == null) { + // Unknown issue? Can happen if you have used a third party detector + // which is no longer available but which left a persistent marker behind + return id1.compareTo(id2); + } + int delta = mList.getSeverity(issue1).ordinal() - + mList.getSeverity(issue2).ordinal(); + if (delta != 0) { + return delta; + } + delta = issue2.getPriority() - issue1.getPriority(); + if (delta != 0) { + return delta; + } + delta = issue1.getCategory().compareTo(issue2.getCategory()); + if (delta != 0) { + return delta; + } + delta = id1.compareTo(id2); + if (delta != 0) { + return delta; + } + + IResource resource1 = marker1.getResource(); + IResource resource2 = marker2.getResource(); + + IProject project1 = resource1.getProject(); + IProject project2 = resource2.getProject(); + delta = project1.getName().compareTo(project2.getName()); + if (delta != 0) { + return delta; + } + + delta = resource1.getName().compareTo(resource2.getName()); + if (delta != 0) { + return delta; + } + + return marker1.getAttribute(IMarker.LINE_NUMBER, 0) + - marker2.getAttribute(IMarker.LINE_NUMBER, 0); + } + } + + static class CategoryColumn extends LintColumn { + + public CategoryColumn(LintList list) { + super(list); + } + + @Override + public @NonNull String getColumnHeaderText() { + return "Category"; + } + + @Override + public int getPreferredCharWidth() { + return 20; + } + + @Override + public String getValue(@NonNull IMarker marker) { + Issue issue = mList.getIssue(marker); + if (issue != null) { + return issue.getCategory().getFullName(); + } else { + return ""; + } + } + } + + static class LocationColumn extends LintColumn { + public LocationColumn(LintList list) { + super(list); + } + + @Override + public @NonNull String getColumnHeaderText() { + return "Location"; + } + + @Override + public int getPreferredCharWidth() { + return 35; + } + + @Override + public String getValue(@NonNull IMarker marker) { + return getStyledValue(marker).toString(); + } + + @Override + public StyledString getStyledValue(@NonNull IMarker marker) { + StyledString styledString = new StyledString(); + + // Combined location + IResource resource = marker.getResource(); + if (resource instanceof IProject) { + styledString.append(resource.getName()); + } else { + // Show location as Parent/File:Line in Project + styledString.append(resource.getName()); + if (resource instanceof IFile) { + int line = marker.getAttribute(IMarker.LINE_NUMBER, -1); + if (line > 1) { + styledString.append(':').append(Integer.toString(line)); + } + } else if (resource instanceof IFolder) { + styledString.append(File.separatorChar); + } + + if (!(resource.getParent() instanceof IProject)) { + styledString.append(" in "); + styledString.append(resource.getParent().getName(), + StyledString.DECORATIONS_STYLER); + } + + styledString.append(String.format(" (%1$s)", resource.getProject().getName()), + StyledString.QUALIFIER_STYLER); + } + + return styledString; + } + + @Override + public int compare(IMarker marker1, IMarker marker2) { + IResource resource1 = marker1.getResource(); + IResource resource2 = marker2.getResource(); + + IProject project1 = resource1.getProject(); + IProject project2 = resource2.getProject(); + int delta = project1.getName().compareTo(project2.getName()); + if (delta != 0) { + return delta; + } + + delta = resource1.getName().compareTo(resource2.getName()); + if (delta != 0) { + return delta; + } + + return marker1.getAttribute(IMarker.LINE_NUMBER, 0) + - marker2.getAttribute(IMarker.LINE_NUMBER, 0); + } + } + + static class FileColumn extends LintColumn { + public FileColumn(LintList list) { + super(list); + } + + @Override + public @NonNull String getColumnHeaderText() { + return "File"; + } + + @Override + public boolean visibleByDefault() { + return false; + } + + @Override + public int getPreferredCharWidth() { + return 12; + } + + @Override + public String getValue(@NonNull IMarker marker) { + if (marker.getResource() instanceof IFile) { + return marker.getResource().getName(); + } else { + return ""; + } + } + } + + static class PathColumn extends LintColumn { + public PathColumn(LintList list) { + super(list); + } + + @Override + public @NonNull String getColumnHeaderText() { + return "Path"; + } + + @Override + public boolean visibleByDefault() { + return false; + } + + @Override + public int getPreferredCharWidth() { + return 25; + } + + @Override + public String getValue(@NonNull IMarker marker) { + return marker.getResource().getFullPath().toOSString(); + } + } + + static class LineColumn extends LintColumn { + public LineColumn(LintList list) { + super(list); + } + + @Override + public @NonNull String getColumnHeaderText() { + return "Line"; + } + + @Override + public boolean visibleByDefault() { + return false; + } + + @Override + public boolean isLeftAligned() { + return false; + } + + @Override + public int getPreferredCharWidth() { + return 4; + } + + @Override + public String getValue(@NonNull IMarker marker) { + int line = getLine(marker); + if (line >= 1) { + return Integer.toString(line); + } else { + return ""; + } + } + + private int getLine(IMarker marker) { + if (marker.getResource() instanceof IFile) { + int line = marker.getAttribute(IMarker.LINE_NUMBER, -1); + return line; + } + + return -1; + } + @Override + public int compare(IMarker marker1, IMarker marker2) { + return getLine(marker1) - getLine(marker2); + } + } + + static class PriorityColumn extends LintColumn { + public PriorityColumn(LintList list) { + super(list); + } + + @Override + public @NonNull String getColumnHeaderText() { + return "Priority"; + } + + @Override + public boolean visibleByDefault() { + return false; + } + + @Override + public boolean isLeftAligned() { + return false; + } + + @Override + public int getPreferredCharWidth() { + return 2; + } + + @Override + public String getValue(@NonNull IMarker marker) { + int priority = getPriority(marker); + if (priority > 0) { + return Integer.toString(priority); + } + return ""; + } + + private int getPriority(IMarker marker) { + Issue issue = mList.getIssue(marker); + if (issue != null) { + return issue.getPriority(); + } + return 0; + } + + @Override + public int compare(IMarker marker1, IMarker marker2) { + return getPriority(marker1) - getPriority(marker2); + } + + @Override + public boolean isAscending() { + return false; + } + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintDeltaProcessor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintDeltaProcessor.java new file mode 100644 index 000000000..ebb9a591c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintDeltaProcessor.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import static com.android.SdkConstants.DOT_CLASS; +import static com.android.SdkConstants.DOT_JAVA; +import static com.android.SdkConstants.EXT_JAVA; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor; +import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarkerDelta; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.swt.widgets.Display; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Delta processor for Java files, which runs single-file lints if it finds that + * the currently active file has been updated. + */ +public class LintDeltaProcessor implements Runnable { + private List<IResource> mFiles; + private IFile mActiveFile; + + private LintDeltaProcessor() { + // Get the active editor file, if any + Display display = AdtPlugin.getDisplay(); + if (display == null || display.isDisposed()) { + return; + } + if (display.getThread() != Thread.currentThread()) { + display.syncExec(this); + } else { + run(); + } + } + + /** + * Creates a new {@link LintDeltaProcessor} + * + * @return a visitor + */ + @NonNull + public static LintDeltaProcessor create() { + return new LintDeltaProcessor(); + } + + /** + * Process the given delta: update lint on any Java source and class files found. + * + * @param delta the delta describing recently changed files + */ + public void process(@NonNull IResourceDelta delta) { + if (mActiveFile == null || !mActiveFile.getName().endsWith(DOT_JAVA)) { + return; + } + + mFiles = new ArrayList<IResource>(); + gatherFiles(delta); + + if (!mFiles.isEmpty()) { + EclipseLintRunner.startLint(mFiles, mActiveFile, null, + false /*fatalOnly*/, false /*show*/); + } + } + + /** + * Process edits in the given file: update lint on the Java source provided + * it's the active file. + * + * @param file the file that was changed + */ + public void process(@NonNull IFile file) { + if (mActiveFile == null || !mActiveFile.getName().endsWith(DOT_JAVA)) { + return; + } + + if (file.equals(mActiveFile)) { + mFiles = Collections.<IResource>singletonList(file); + EclipseLintRunner.startLint(mFiles, mActiveFile, null, + false /*fatalOnly*/, false /*show*/); + } + } + + /** + * Collect .java and .class files to be run in lint. Only collects files + * that match the active editor. + */ + private void gatherFiles(@NonNull IResourceDelta delta) { + IResource resource = delta.getResource(); + String name = resource.getName(); + if (name.endsWith(DOT_JAVA)) { + if (resource.equals(mActiveFile)) { + mFiles.add(resource); + } + } else if (name.endsWith(DOT_CLASS)) { + // Make sure this class corresponds to the .java file, meaning it has + // the same basename, or that it is an inner class of a class that + // matches the same basename. (We could potentially make sure the package + // names match too, but it's unlikely that the class names match without a + // package match, and there's no harm in including some extra classes here, + // since lint will resolve full paths and the resource markers won't go + // to the wrong place, we simply end up analyzing some extra files.) + String className = mActiveFile.getName(); + if (name.regionMatches(0, className, 0, className.length() - DOT_JAVA.length())) { + if (name.length() == className.length() - DOT_JAVA.length() + DOT_CLASS.length() + || name.charAt(className.length() - DOT_JAVA.length()) == '$') { + mFiles.add(resource); + } + } + } else { + IResourceDelta[] children = delta.getAffectedChildren(); + if (children != null && children.length > 0) { + for (IResourceDelta d : children) { + gatherFiles(d); + } + } + } + } + + @Override + public void run() { + // Get the active file: this must be run on the GUI thread + mActiveFile = AdtUtils.getActiveFile(); + } + + /** + * Start listening to the resource monitor + * + * @param resourceMonitor the resource monitor + */ + public static void startListening(@NonNull GlobalProjectMonitor resourceMonitor) { + // Add a file listener which finds out when files have changed. This is listening + // specifically for saves of Java files, in order to run incremental lint on them. + // Note that the {@link PostCompilerBuilder} already handles incremental lint files + // on Java files - and runs it for both the .java and .class files. + // + // However, if Project > Build Automatically is turned off, then the PostCompilerBuilder + // isn't run after a save. THAT's what the below is for: it will run and *only* + // run lint incrementally if build automatically is off. + assert sListener == null; // Should only be called once on plugin activation + sListener = new IFileListener() { + @Override + public void fileChanged(@NonNull IFile file, + @NonNull IMarkerDelta[] markerDeltas, + int kind, @Nullable String extension, int flags, boolean isAndroidProject) { + if (!isAndroidProject || flags == IResourceDelta.MARKERS) { + // If not an Android project or ONLY the markers changed. + // Ignore these since they happen + // when we add markers for lint errors found in the current file, + // which would cause us to repeatedly enter this method over and over + // again. + return; + } + if (EXT_JAVA.equals(extension) + && !ResourceManager.isAutoBuilding() + && AdtPrefs.getPrefs().isLintOnSave()) { + LintDeltaProcessor.create().process(file); + } + } + }; + resourceMonitor.addFileListener(sListener, IResourceDelta.ADDED | IResourceDelta.CHANGED); + } + + /** + * Stop listening to the resource monitor + * + * @param resourceMonitor the resource monitor + */ + public static void stopListening(@NonNull GlobalProjectMonitor resourceMonitor) { + assert sListener != null; + resourceMonitor.removeFileListener(sListener); + sListener = null; + } + + private static IFileListener sListener; +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintEditAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintEditAction.java new file mode 100644 index 000000000..bf05ce0b1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintEditAction.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import com.android.annotations.NonNull; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DelegatingAction; + +import org.eclipse.jface.action.IAction; +import org.eclipse.swt.widgets.Event; + +/** + * Action intended to wrap an existing XML editor action, and then runs lint after + * the edit. + */ +public class LintEditAction extends DelegatingAction { + private final AndroidXmlEditor mEditor; + + /** + * Creates a new {@link LintEditAction} associated with the given editor to + * wrap the given action + * + * @param action the action to be wrapped + * @param editor the editor associated with the action + */ + public LintEditAction(@NonNull IAction action, @NonNull AndroidXmlEditor editor) { + super(action); + mEditor = editor; + } + + @Override + public void runWithEvent(Event event) { + super.runWithEvent(event); + mEditor.runEditHooks(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFix.java new file mode 100644 index 000000000..366e94945 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFix.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.tools.lint.checks.AccessibilityDetector; +import com.android.tools.lint.checks.DetectMissingPrefix; +import com.android.tools.lint.checks.DosLineEndingDetector; +import com.android.tools.lint.checks.HardcodedValuesDetector; +import com.android.tools.lint.checks.InefficientWeightDetector; +import com.android.tools.lint.checks.ManifestDetector; +import com.android.tools.lint.checks.MissingIdDetector; +import com.android.tools.lint.checks.ObsoleteLayoutParamsDetector; +import com.android.tools.lint.checks.PxUsageDetector; +import com.android.tools.lint.checks.ScrollViewChildDetector; +import com.android.tools.lint.checks.SecurityDetector; +import com.android.tools.lint.checks.TextFieldDetector; +import com.android.tools.lint.checks.TranslationDetector; +import com.android.tools.lint.checks.TypoDetector; +import com.android.tools.lint.checks.TypographyDetector; +import com.android.tools.lint.checks.UseCompoundDrawableDetector; +import com.android.tools.lint.checks.UselessViewDetector; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.TextFormat; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.contentassist.ICompletionProposal; +import org.eclipse.jface.text.contentassist.IContextInformation; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; + +import java.lang.reflect.Constructor; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +abstract class LintFix implements ICompletionProposal { + protected final IMarker mMarker; + protected final String mId; + + protected LintFix(String id, IMarker marker) { + mId = id; + mMarker = marker; + } + + /** + * Returns true if this fix needs focus (which means that when the fix is + * performed from for example a {@link LintListDialog}'s Fix button) the + * editor needs to be given focus. + * + * @return true if this fix needs focus after being applied + */ + public boolean needsFocus() { + return true; + } + + /** + * Returns true if this fix can be performed along side other fixes + * + * @return true if this fix can be performed in a bulk operation with other + * fixes + */ + public boolean isBulkCapable() { + return false; + } + + /** + * Returns true if this fix can be cancelled once it's invoked. This is the case + * for fixes which shows a confirmation dialog (such as the Extract String etc). + * This will be used to determine whether the marker can be deleted immediately + * (for non-cancelable fixes) or if it should be left alone and detected fix + * on the next save. + * + * @return true if the fix can be cancelled + */ + public boolean isCancelable() { + return true; + } + + // ---- Implements ICompletionProposal ---- + + @Override + public String getDisplayString() { + return null; + } + + @Override + public String getAdditionalProposalInfo() { + Issue issue = EclipseLintClient.getRegistry().getIssue(mId); + if (issue != null) { + return issue.getExplanation(TextFormat.HTML); + } + + return null; + } + + public void deleteMarker() { + try { + mMarker.delete(); + } catch (PartInitException e) { + AdtPlugin.log(e, null); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + + @Override + public Point getSelection(IDocument document) { + return null; + } + + @Override + public Image getImage() { + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + return sharedImages.getImage(ISharedImages.IMG_OBJS_WARN_TSK); + } + + @Override + public IContextInformation getContextInformation() { + return null; + } + + // --- Access to available fixes --- + + private static final Map<String, Class<? extends LintFix>> sFixes = + new HashMap<String, Class<? extends LintFix>>(); + // Keep this map in sync with BuiltinIssueRegistry's hasAutoFix() data + static { + sFixes.put(InefficientWeightDetector.INEFFICIENT_WEIGHT.getId(), + LinearLayoutWeightFix.class); + sFixes.put(AccessibilityDetector.ISSUE.getId(), SetAttributeFix.class); + sFixes.put(InefficientWeightDetector.BASELINE_WEIGHTS.getId(), SetAttributeFix.class); + sFixes.put(ManifestDetector.ALLOW_BACKUP.getId(), SetAttributeFix.class); + sFixes.put(MissingIdDetector.ISSUE.getId(), SetAttributeFix.class); + sFixes.put(HardcodedValuesDetector.ISSUE.getId(), ExtractStringFix.class); + sFixes.put(UselessViewDetector.USELESS_LEAF.getId(), RemoveUselessViewFix.class); + sFixes.put(UselessViewDetector.USELESS_PARENT.getId(), RemoveUselessViewFix.class); + sFixes.put(PxUsageDetector.PX_ISSUE.getId(), ConvertToDpFix.class); + sFixes.put(TextFieldDetector.ISSUE.getId(), SetAttributeFix.class); + sFixes.put(SecurityDetector.EXPORTED_SERVICE.getId(), SetAttributeFix.class); + sFixes.put(TranslationDetector.MISSING.getId(), SetAttributeFix.class); + sFixes.put(DetectMissingPrefix.MISSING_NAMESPACE.getId(), AddPrefixFix.class); + sFixes.put(ScrollViewChildDetector.ISSUE.getId(), SetScrollViewSizeFix.class); + sFixes.put(ObsoleteLayoutParamsDetector.ISSUE.getId(), ObsoleteLayoutParamsFix.class); + sFixes.put(TypographyDetector.DASHES.getId(), TypographyFix.class); + sFixes.put(TypographyDetector.ELLIPSIS.getId(), TypographyFix.class); + sFixes.put(TypographyDetector.FRACTIONS.getId(), TypographyFix.class); + sFixes.put(TypographyDetector.OTHER.getId(), TypographyFix.class); + sFixes.put(TypographyDetector.QUOTES.getId(), TypographyFix.class); + sFixes.put(UseCompoundDrawableDetector.ISSUE.getId(), + UseCompoundDrawableDetectorFix.class); + sFixes.put(TypoDetector.ISSUE.getId(), TypoFix.class); + sFixes.put(DosLineEndingDetector.ISSUE.getId(), DosLineEndingsFix.class); + // ApiDetector.UNSUPPORTED is provided as a marker resolution rather than + // a quick assistant (the marker resolution adds a suitable @TargetApi annotation) + } + + public static boolean hasFix(String id) { + return sFixes.containsKey(id); + } + + /** + * Returns one or more fixes for the given issue, or null if no fixes are available + * + * @param id the id o the issue to obtain a fix for (see {@link Issue#getId()}) + * @param marker the marker corresponding to the error + * @return a nonempty list of fix, or null + */ + @Nullable + public static List<LintFix> getFixes(@NonNull String id, @NonNull IMarker marker) { + Class<? extends LintFix> clazz = sFixes.get(id); + if (clazz != null) { + try { + Constructor<? extends LintFix> constructor = clazz.getDeclaredConstructor( + String.class, IMarker.class); + constructor.setAccessible(true); + LintFix fix = constructor.newInstance(id, marker); + List<LintFix> alternatives = fix.getAllFixes(); + if (alternatives != null) { + return alternatives; + } else { + return Collections.singletonList(fix); + } + } catch (Throwable t) { + AdtPlugin.log(t, null); + } + } + + return null; + } + + /** + * Returns a full list of fixes for this issue. This will produce a list of + * multiple fixes, in the desired order, which provide alternative ways of + * fixing the issue. + * + * @return a list of fixes to fix this issue, or null if there are no + * variations + */ + @Nullable + protected List<LintFix> getAllFixes() { + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFixGenerator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFixGenerator.java new file mode 100644 index 000000000..da100850a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFixGenerator.java @@ -0,0 +1,563 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import static com.android.SdkConstants.DOT_JAVA; +import static com.android.SdkConstants.DOT_XML; + +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.tools.lint.client.api.Configuration; +import com.android.tools.lint.client.api.DefaultConfiguration; +import com.android.tools.lint.client.api.IssueRegistry; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.TextFormat; +import com.android.tools.lint.detector.api.Project; +import com.android.tools.lint.detector.api.Severity; +import com.android.utils.SdkUtils; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Region; +import org.eclipse.jface.text.contentassist.ICompletionProposal; +import org.eclipse.jface.text.contentassist.IContextInformation; +import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext; +import org.eclipse.jface.text.quickassist.IQuickAssistProcessor; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.ISourceViewer; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IMarkerResolution; +import org.eclipse.ui.IMarkerResolution2; +import org.eclipse.ui.IMarkerResolutionGenerator2; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.part.FileEditorInput; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A quickfix and marker resolution for disabling lint checks, and any + * IDE specific implementations for fixing the warnings. + * <p> + * I would really like for this quickfix to show up as a light bulb on top of the error + * icon in the editor, and I've spent a whole day trying to make it work. I did not + * succeed, but here are the steps I tried in case I want to pick up the work again + * later: + * <ul> + * <li> + * The WST has some support for quick fixes, and I came across some forum posts + * referencing the ability to show light bulbs. However, it turns out that the + * quickfix support for annotations in WST is hardcoded to source validation + * errors *only*. + * <li> + * I tried defining my own editor annotations, and customizing the icon directly + * by either setting an icon or using the image provider. This works fine + * if I make my marker be a new independent marker type. However, whenever I + * switch the marker type back to extend the "Problem" type, then the icon reverts + * back to the standard error icon and it ignores my custom settings. + * And if I switch away from the Problems marker type, then the errors no longer + * show up in the Problems view. (I also tried extending the JDT marker but that + * still didn't work.) + * <li> + * It looks like only JDT handles quickfix icons. It has a bunch of custom code + * to handle this, along with its own Annotation subclass used by the editor. + * I tried duplicating some of this by subclassing StructuredTextEditor, but + * it was evident that I'd have to pull in a *huge* amount of duplicated code to + * make this work, which seems risky given that all this is internal code that + * can change from one Eclipse version to the next. + * </ul> + * It looks like our best bet would be to reconsider whether these should show up + * in the Problems view; perhaps we should use a custom view for these. That would also + * make marker management more obvious. + */ +@SuppressWarnings("restriction") // DOM model +public class LintFixGenerator implements IMarkerResolutionGenerator2, IQuickAssistProcessor { + /** Constructs a new {@link LintFixGenerator} */ + public LintFixGenerator() { + } + + // ---- Implements IMarkerResolutionGenerator2 ---- + + @Override + public boolean hasResolutions(IMarker marker) { + try { + assert marker.getType().equals(AdtConstants.MARKER_LINT); + } catch (CoreException e) { + } + + return true; + } + + @Override + public IMarkerResolution[] getResolutions(IMarker marker) { + String id = marker.getAttribute(EclipseLintRunner.MARKER_CHECKID_PROPERTY, + ""); //$NON-NLS-1$ + IResource resource = marker.getResource(); + + List<IMarkerResolution> resolutions = new ArrayList<IMarkerResolution>(); + + if (resource.getName().endsWith(DOT_JAVA)) { + AddSuppressAnnotation.createFixes(marker, id, resolutions); + } + + resolutions.add(new MoreInfoProposal(id, marker.getAttribute(IMarker.MESSAGE, null))); + resolutions.add(new SuppressProposal(resource, id, false)); + resolutions.add(new SuppressProposal(resource.getProject(), id, true /* all */)); + resolutions.add(new SuppressProposal(resource, id, true /* all */)); + resolutions.add(new ClearMarkersProposal(resource, true /* all */)); + + if (resolutions.size() > 0) { + return resolutions.toArray(new IMarkerResolution[resolutions.size()]); + } + + return null; + } + + // ---- Implements IQuickAssistProcessor ---- + + @Override + public String getErrorMessage() { + return "Disable Lint Error"; + } + + @Override + public boolean canFix(Annotation annotation) { + return true; + } + + @Override + public boolean canAssist(IQuickAssistInvocationContext invocationContext) { + return true; + } + + @Override + public ICompletionProposal[] computeQuickAssistProposals( + IQuickAssistInvocationContext invocationContext) { + ISourceViewer sourceViewer = invocationContext.getSourceViewer(); + AndroidXmlEditor editor = AndroidXmlEditor.fromTextViewer(sourceViewer); + if (editor != null) { + IFile file = editor.getInputFile(); + if (file == null) { + return null; + } + IDocument document = sourceViewer.getDocument(); + List<IMarker> markers = AdtUtils.findMarkersOnLine(AdtConstants.MARKER_LINT, + file, document, invocationContext.getOffset()); + List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); + if (markers.size() > 0) { + for (IMarker marker : markers) { + String id = marker.getAttribute(EclipseLintRunner.MARKER_CHECKID_PROPERTY, + ""); //$NON-NLS-1$ + + // TODO: Allow for more than one fix? + List<LintFix> fixes = LintFix.getFixes(id, marker); + if (fixes != null) { + for (LintFix fix : fixes) { + proposals.add(fix); + } + } + + String message = marker.getAttribute(IMarker.MESSAGE, null); + proposals.add(new MoreInfoProposal(id, message)); + + proposals.addAll(AddSuppressAttribute.createFixes(editor, marker, id)); + proposals.add(new SuppressProposal(file, id, false)); + proposals.add(new SuppressProposal(file.getProject(), id, true /* all */)); + proposals.add(new SuppressProposal(file, id, true /* all */)); + + proposals.add(new ClearMarkersProposal(file, true /* all */)); + } + } + if (proposals.size() > 0) { + return proposals.toArray(new ICompletionProposal[proposals.size()]); + } + } + + return null; + } + + /** + * Suppress the given detector, and rerun the checks on the file + * + * @param id the id of the detector to be suppressed, or null + * @param updateMarkers if true, update all markers + * @param resource the resource associated with the markers + * @param thisFileOnly if true, only suppress this issue in this file + */ + public static void suppressDetector(String id, boolean updateMarkers, IResource resource, + boolean thisFileOnly) { + IssueRegistry registry = EclipseLintClient.getRegistry(); + Issue issue = registry.getIssue(id); + if (issue != null) { + EclipseLintClient mClient = new EclipseLintClient(registry, + Collections.singletonList(resource), null, false); + Project project = null; + IProject eclipseProject = resource.getProject(); + if (eclipseProject != null) { + File dir = AdtUtils.getAbsolutePath(eclipseProject).toFile(); + project = mClient.getProject(dir, dir); + } + Configuration configuration = mClient.getConfigurationFor(project); + if (thisFileOnly && configuration instanceof DefaultConfiguration) { + File file = AdtUtils.getAbsolutePath(resource).toFile(); + ((DefaultConfiguration) configuration).ignore(issue, file); + } else { + configuration.setSeverity(issue, Severity.IGNORE); + } + } + + if (updateMarkers) { + EclipseLintClient.removeMarkers(resource, id); + } + } + + /** + * Adds a suppress lint annotation or attribute depending on whether the + * error is in a Java or XML file. + * + * @param marker the marker pointing to the error to be suppressed + */ + public static void addSuppressAnnotation(IMarker marker) { + String id = EclipseLintClient.getId(marker); + if (id != null) { + IResource resource = marker.getResource(); + if (!(resource instanceof IFile)) { + return; + } + IFile file = (IFile) resource; + boolean isJava = file.getName().endsWith(DOT_JAVA); + boolean isXml = SdkUtils.endsWith(file.getName(), DOT_XML); + if (!isJava && !isXml) { + return; + } + + try { + // See if the current active file is the one containing this marker; + // if so we can take some shortcuts + IEditorPart activeEditor = AdtUtils.getActiveEditor(); + IEditorPart part = null; + if (activeEditor != null) { + IEditorInput input = activeEditor.getEditorInput(); + if (input instanceof FileEditorInput + && ((FileEditorInput)input).getFile().equals(file)) { + part = activeEditor; + } + } + if (part == null) { + IRegion region = null; + int start = marker.getAttribute(IMarker.CHAR_START, -1); + int end = marker.getAttribute(IMarker.CHAR_END, -1); + if (start != -1 && end != -1) { + region = new Region(start, end - start); + } + part = AdtPlugin.openFile(file, region, true /* showEditor */); + } + + if (isJava) { + List<IMarkerResolution> resolutions = new ArrayList<IMarkerResolution>(); + AddSuppressAnnotation.createFixes(marker, id, resolutions); + if (resolutions.size() > 0) { + resolutions.get(0).run(marker); + } + } else { + assert isXml; + if (part instanceof AndroidXmlEditor) { + AndroidXmlEditor editor = (AndroidXmlEditor) part; + List<AddSuppressAttribute> fixes = AddSuppressAttribute.createFixes(editor, + marker, id); + if (fixes.size() > 0) { + IStructuredDocument document = editor.getStructuredDocument(); + fixes.get(0).apply(document); + } + } + } + } catch (PartInitException pie) { + AdtPlugin.log(pie, null); + } + } + } + + private static class SuppressProposal implements ICompletionProposal, IMarkerResolution2 { + private final String mId; + private final boolean mGlobal; + private final IResource mResource; + + private SuppressProposal(IResource resource, String check, boolean global) { + mResource = resource; + mId = check; + mGlobal = global; + } + + private void perform() { + suppressDetector(mId, true, mResource, !mGlobal); + } + + @Override + public String getDisplayString() { + if (mResource instanceof IProject) { + return "Disable Check in This Project"; + } else if (mGlobal) { + return "Disable Check"; + } else { + return "Disable Check in This File Only"; + } + } + + // ---- Implements MarkerResolution2 ---- + + @Override + public String getLabel() { + return getDisplayString(); + } + + @Override + public void run(IMarker marker) { + perform(); + } + + @Override + public String getDescription() { + return getAdditionalProposalInfo(); + } + + // ---- Implements ICompletionProposal ---- + + @Override + public void apply(IDocument document) { + perform(); + } + + @Override + public Point getSelection(IDocument document) { + return null; + } + + @Override + public String getAdditionalProposalInfo() { + StringBuilder sb = new StringBuilder(200); + if (mResource instanceof IProject) { + sb.append("Suppresses this type of lint warning in the current project only."); + } else if (mGlobal) { + sb.append("Suppresses this type of lint warning in all files."); + } else { + sb.append("Suppresses this type of lint warning in the current file only."); + } + sb.append("<br><br>"); //$NON-NLS-1$ + sb.append("You can re-enable checks from the \"Android > Lint Error Checking\" preference page."); + + return sb.toString(); + } + + @Override + public Image getImage() { + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + return sharedImages.getImage(ISharedImages.IMG_OBJS_WARN_TSK); + } + + @Override + public IContextInformation getContextInformation() { + return null; + } + } + + private static class ClearMarkersProposal implements ICompletionProposal, IMarkerResolution2 { + private final boolean mGlobal; + private final IResource mResource; + + public ClearMarkersProposal(IResource resource, boolean global) { + mResource = resource; + mGlobal = global; + } + + private void perform() { + IResource resource = mGlobal ? mResource.getProject() : mResource; + EclipseLintClient.clearMarkers(resource); + } + + @Override + public String getDisplayString() { + return mGlobal ? "Clear All Lint Markers" : "Clear Markers in This File Only"; + } + + // ---- Implements MarkerResolution2 ---- + + @Override + public String getLabel() { + return getDisplayString(); + } + + @Override + public void run(IMarker marker) { + perform(); + } + + @Override + public String getDescription() { + return getAdditionalProposalInfo(); + } + + // ---- Implements ICompletionProposal ---- + + @Override + public void apply(IDocument document) { + perform(); + } + + @Override + public Point getSelection(IDocument document) { + return null; + } + + @Override + public String getAdditionalProposalInfo() { + StringBuilder sb = new StringBuilder(200); + if (mGlobal) { + sb.append("Clears all lint warning markers from the project."); + } else { + sb.append("Clears all lint warnings from this file."); + } + sb.append("<br><br>"); //$NON-NLS-1$ + sb.append("This temporarily hides the problem, but does not suppress it. " + + "Running Lint again can bring the error back."); + if (AdtPrefs.getPrefs().isLintOnSave()) { + sb.append(' '); + sb.append("This will happen the next time the file is saved since lint-on-save " + + "is enabled. You can turn this off in the \"Lint Error Checking\" " + + "preference page."); + } + + return sb.toString(); + } + + @Override + public Image getImage() { + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + return sharedImages.getImage(ISharedImages.IMG_ELCL_REMOVE); + } + + @Override + public IContextInformation getContextInformation() { + return null; + } + } + + private static class MoreInfoProposal implements ICompletionProposal, IMarkerResolution2 { + private final String mId; + private final String mMessage; + + public MoreInfoProposal(String id, String message) { + mId = id; + mMessage = message; + } + + private void perform() { + Issue issue = EclipseLintClient.getRegistry().getIssue(mId); + assert issue != null : mId; + + StringBuilder sb = new StringBuilder(300); + sb.append(mMessage); + sb.append('\n').append('\n'); + sb.append("Issue Explanation:"); + sb.append('\n'); + String explanation = issue.getExplanation(TextFormat.TEXT); + if (explanation != null && !explanation.isEmpty()) { + sb.append('\n'); + sb.append(explanation); + } else { + sb.append(issue.getBriefDescription(TextFormat.TEXT)); + } + + if (issue.getMoreInfo() != null) { + sb.append('\n').append('\n'); + sb.append("More Information: "); + sb.append(issue.getMoreInfo()); + } + + MessageDialog.openInformation(AdtPlugin.getShell(), "More Info", + sb.toString()); + } + + @Override + public String getDisplayString() { + return String.format("Explain Issue (%1$s)", mId); + } + + // ---- Implements MarkerResolution2 ---- + + @Override + public String getLabel() { + return getDisplayString(); + } + + @Override + public void run(IMarker marker) { + perform(); + } + + @Override + public String getDescription() { + return getAdditionalProposalInfo(); + } + + // ---- Implements ICompletionProposal ---- + + @Override + public void apply(IDocument document) { + perform(); + } + + @Override + public Point getSelection(IDocument document) { + return null; + } + + @Override + public String getAdditionalProposalInfo() { + return "Provides more information about this issue." + + "<br><br>" //$NON-NLS-1$ + + EclipseLintClient.getRegistry().getIssue(mId).getExplanation( + TextFormat.HTML); + } + + @Override + public Image getImage() { + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + return sharedImages.getImage(ISharedImages.IMG_OBJS_INFO_TSK); + } + + @Override + public IContextInformation getContextInformation() { + return null; + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintJob.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintJob.java new file mode 100644 index 000000000..51fa2d145 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintJob.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import static com.android.SdkConstants.DOT_CLASS; +import static com.android.SdkConstants.DOT_JAVA; +import static com.android.SdkConstants.DOT_XML; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.tools.lint.client.api.IssueRegistry; +import com.android.tools.lint.client.api.LintDriver; +import com.android.tools.lint.client.api.LintRequest; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.Scope; +import com.android.utils.SdkUtils; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.IJobManager; +import org.eclipse.core.runtime.jobs.Job; + +import java.io.File; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +/** Job to check lint on a set of resources */ +public final class LintJob extends Job { + /** Job family */ + private static final Object FAMILY_RUN_LINT = new Object(); + private final EclipseLintClient mClient; + private final List<? extends IResource> mResources; + private final IResource mSource; + private final IssueRegistry mRegistry; + private LintDriver mLint; + private boolean mFatal; + + public LintJob( + @NonNull EclipseLintClient client, + @NonNull List<? extends IResource> resources, + @Nullable IResource source, + @NonNull IssueRegistry registry) { + super("Running Android Lint"); + mClient = client; + mResources = resources; + mSource = source; + mRegistry = registry; + } + + public LintJob( + @NonNull EclipseLintClient client, + @NonNull List<? extends IResource> resources, + @Nullable IResource source) { + this(client, resources, source, EclipseLintClient.getRegistry()); + } + + @Override + public boolean belongsTo(Object family) { + return family == FAMILY_RUN_LINT; + } + + @Override + protected void canceling() { + super.canceling(); + if (mLint != null) { + mLint.cancel(); + } + } + + @Override + @NonNull + protected IStatus run(IProgressMonitor monitor) { + try { + monitor.beginTask("Looking for errors", IProgressMonitor.UNKNOWN); + EnumSet<Scope> scope = null; + List<File> files = new ArrayList<File>(mResources.size()); + for (IResource resource : mResources) { + File file = AdtUtils.getAbsolutePath(resource).toFile(); + files.add(file); + + if (resource instanceof IProject && mSource == null) { + scope = Scope.ALL; + } else { + String name = resource.getName(); + if (SdkUtils.endsWithIgnoreCase(name, DOT_XML)) { + if (name.equals(SdkConstants.FN_ANDROID_MANIFEST_XML)) { + scope = EnumSet.of(Scope.MANIFEST); + } else { + scope = Scope.RESOURCE_FILE_SCOPE; + } + } else if (name.endsWith(DOT_JAVA) && resource instanceof IFile) { + if (scope != null) { + if (!scope.contains(Scope.JAVA_FILE)) { + scope = EnumSet.copyOf(scope); + scope.add(Scope.JAVA_FILE); + } + } else { + scope = Scope.JAVA_FILE_SCOPE; + } + } else if (name.endsWith(DOT_CLASS) && resource instanceof IFile) { + if (scope != null) { + if (!scope.contains(Scope.CLASS_FILE)) { + scope = EnumSet.copyOf(scope); + scope.add(Scope.CLASS_FILE); + } + } else { + scope = Scope.CLASS_FILE_SCOPE; + } + } else { + return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, Status.ERROR, + "Only XML & Java files are supported for single file lint", null); //$NON-NLS-1$ + } + } + } + if (scope == null) { + scope = Scope.ALL; + } + if (mSource == null) { + assert !Scope.checkSingleFile(scope) : scope + " with " + mResources; + } + // Check single file? + if (mSource != null) { + // Delete specific markers + IMarker[] markers = EclipseLintClient.getMarkers(mSource); + for (IMarker marker : markers) { + String id = marker.getAttribute(EclipseLintRunner.MARKER_CHECKID_PROPERTY, ""); + Issue issue = mRegistry.getIssue(id); + if (issue == null) { + continue; + } + if (issue.getImplementation().isAdequate(scope)) { + marker.delete(); + } + } + mClient.setSearchForSuperClasses(true); + } else { + EclipseLintClient.clearMarkers(mResources); + } + + mLint = new LintDriver(mRegistry, mClient); + mLint.analyze(new LintRequest(mClient, files).setScope(scope)); + mFatal = mClient.hasFatalErrors(); + return Status.OK_STATUS; + } catch (Exception e) { + return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, Status.ERROR, + "Failed", e); //$NON-NLS-1$ + } finally { + if (monitor != null) { + monitor.done(); + } + } + } + + /** + * Returns true if a fatal error was encountered + * + * @return true if a fatal error was encountered + */ + public boolean isFatal() { + return mFatal; + } + + /** + * Returns the associated lint client + * + * @return the associated lint client + */ + @NonNull + public EclipseLintClient getLintClient() { + return mClient; + } + + /** Returns the current lint jobs, if any (never returns null but array may be empty) */ + @NonNull + static Job[] getCurrentJobs() { + IJobManager jobManager = Job.getJobManager(); + return jobManager.find(LintJob.FAMILY_RUN_LINT); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintList.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintList.java new file mode 100644 index 000000000..ccb04bb6b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintList.java @@ -0,0 +1,979 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutActionBar; +import com.android.tools.lint.client.api.Configuration; +import com.android.tools.lint.client.api.IssueRegistry; +import com.android.tools.lint.client.api.LintClient; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.Severity; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IMarkerDelta; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.viewers.ColumnPixelData; +import org.eclipse.jface.viewers.ColumnWeightData; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.StyledCellLabelProvider; +import org.eclipse.jface.viewers.StyledString; +import org.eclipse.jface.viewers.TableLayout; +import org.eclipse.jface.viewers.TreeNodeContentProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.TreeViewerColumn; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.jface.viewers.ViewerComparator; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.BusyIndicator; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.events.TreeEvent; +import org.eclipse.swt.events.TreeListener; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; +import org.eclipse.swt.widgets.TreeItem; +import org.eclipse.ui.IMemento; +import org.eclipse.ui.IWorkbenchPartSite; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.progress.IWorkbenchSiteProgressService; +import org.eclipse.ui.progress.WorkbenchJob; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A tree-table widget which shows a list of lint warnings for an underlying + * {@link IResource} such as a file, a project, or a list of projects. + */ +class LintList extends Composite implements IResourceChangeListener, ControlListener { + private static final Object UPDATE_MARKERS_FAMILY = new Object(); + + // For persistence: + private static final String KEY_WIDTHS = "lintColWidth"; //$NON-NLS-1$ + private static final String KEY_VISIBLE = "lintColVisible"; //$NON-NLS-1$ + // Mapping SWT TreeColumns to LintColumns + private static final String KEY_COLUMN = "lintColumn"; //$NON-NLS-1$ + + private final IWorkbenchPartSite mSite; + private final TreeViewer mTreeViewer; + private final Tree mTree; + private Set<String> mExpandedIds; + private ContentProvider mContentProvider; + private String mSelectedId; + private List<? extends IResource> mResources; + private Configuration mConfiguration; + private final boolean mSingleFile; + private int mErrorCount; + private int mWarningCount; + private final UpdateMarkersJob mUpdateMarkersJob = new UpdateMarkersJob(); + private final IssueRegistry mRegistry; + private final IMemento mMemento; + private final LintColumn mMessageColumn = new LintColumn.MessageColumn(this); + private final LintColumn mLineColumn = new LintColumn.LineColumn(this); + private final LintColumn[] mColumns = new LintColumn[] { + mMessageColumn, + new LintColumn.PriorityColumn(this), + new LintColumn.CategoryColumn(this), + new LintColumn.LocationColumn(this), + new LintColumn.FileColumn(this), + new LintColumn.PathColumn(this), + mLineColumn + }; + private LintColumn[] mVisibleColumns; + + LintList(IWorkbenchPartSite site, Composite parent, IMemento memento, boolean singleFile) { + super(parent, SWT.NONE); + mSingleFile = singleFile; + mMemento = memento; + mSite = site; + mRegistry = EclipseLintClient.getRegistry(); + + GridLayout gridLayout = new GridLayout(1, false); + gridLayout.marginWidth = 0; + gridLayout.marginHeight = 0; + setLayout(gridLayout); + + mTreeViewer = new TreeViewer(this, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI); + mTree = mTreeViewer.getTree(); + mTree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); + + createColumns(); + mTreeViewer.setComparator(new TableComparator()); + setSortIndicators(); + + mContentProvider = new ContentProvider(); + mTreeViewer.setContentProvider(mContentProvider); + + mTree.setLinesVisible(true); + mTree.setHeaderVisible(true); + mTree.addControlListener(this); + + ResourcesPlugin.getWorkspace().addResourceChangeListener( + this, + IResourceChangeEvent.POST_CHANGE + | IResourceChangeEvent.PRE_BUILD + | IResourceChangeEvent.POST_BUILD); + + // Workaround for https://bugs.eclipse.org/341865 + mTree.addPaintListener(new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + mTreePainted = true; + mTreeViewer.getTree().removePaintListener(this); + } + }); + + // Remember the most recently selected id category such that we can + // attempt to reselect it after a refresh + mTree.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + List<IMarker> markers = getSelectedMarkers(); + if (markers.size() > 0) { + mSelectedId = EclipseLintClient.getId(markers.get(0)); + } + } + }); + mTree.addTreeListener(new TreeListener() { + @Override + public void treeExpanded(TreeEvent e) { + Object data = e.item.getData(); + if (data instanceof IMarker) { + String id = EclipseLintClient.getId((IMarker) data); + if (id != null) { + if (mExpandedIds == null) { + mExpandedIds = new HashSet<String>(); + } + mExpandedIds.add(id); + } + } + } + + @Override + public void treeCollapsed(TreeEvent e) { + if (mExpandedIds != null) { + Object data = e.item.getData(); + if (data instanceof IMarker) { + String id = EclipseLintClient.getId((IMarker) data); + if (id != null) { + mExpandedIds.remove(id); + } + } + } + } + }); + } + + private boolean mTreePainted; + + private void updateColumnWidths() { + Rectangle r = mTree.getClientArea(); + int availableWidth = r.width; + // Add all available size to the first column + for (int i = 1; i < mTree.getColumnCount(); i++) { + TreeColumn column = mTree.getColumn(i); + availableWidth -= column.getWidth(); + } + if (availableWidth > 100) { + mTree.getColumn(0).setWidth(availableWidth); + } + } + + public void setResources(List<? extends IResource> resources) { + mResources = resources; + + mConfiguration = null; + for (IResource resource : mResources) { + IProject project = resource.getProject(); + if (project != null) { + // For logging only + LintClient client = new EclipseLintClient(null, null, null, false); + mConfiguration = ProjectLintConfiguration.get(client, project, false); + break; + } + } + if (mConfiguration == null) { + mConfiguration = GlobalLintConfiguration.get(); + } + + List<IMarker> markerList = getMarkers(); + mTreeViewer.setInput(markerList); + if (mSingleFile) { + expandAll(); + } + + // Selecting the first item isn't a good idea since it may not be the first + // item shown in the table (since it does its own sorting), and furthermore we + // may not have all the data yet; this is called when scanning begins, not when + // it's done: + //if (mTree.getItemCount() > 0) { + // mTree.select(mTree.getItem(0)); + //} + + updateColumnWidths(); // in case mSingleFile changed + } + + /** Select the first item */ + public void selectFirst() { + if (mTree.getItemCount() > 0) { + mTree.select(mTree.getItem(0)); + } + } + + private List<IMarker> getMarkers() { + mErrorCount = mWarningCount = 0; + List<IMarker> markerList = new ArrayList<IMarker>(); + if (mResources != null) { + for (IResource resource : mResources) { + IMarker[] markers = EclipseLintClient.getMarkers(resource); + for (IMarker marker : markers) { + markerList.add(marker); + int severity = marker.getAttribute(IMarker.SEVERITY, 0); + if (severity == IMarker.SEVERITY_ERROR) { + mErrorCount++; + } else if (severity == IMarker.SEVERITY_WARNING) { + mWarningCount++; + } + } + } + + // No need to sort the marker list here; it will be sorted by the tree table model + } + return markerList; + } + + public int getErrorCount() { + return mErrorCount; + } + + public int getWarningCount() { + return mWarningCount; + } + + @Override + protected void checkSubclass() { + // Disable the check that prevents subclassing of SWT components + } + + public void addSelectionListener(SelectionListener listener) { + mTree.addSelectionListener(listener); + } + + public void refresh() { + mTreeViewer.refresh(); + } + + public List<IMarker> getSelectedMarkers() { + TreeItem[] selection = mTree.getSelection(); + List<IMarker> markers = new ArrayList<IMarker>(selection.length); + for (TreeItem item : selection) { + Object data = item.getData(); + if (data instanceof IMarker) { + markers.add((IMarker) data); + } + } + + return markers; + } + + @Override + public void dispose() { + cancelJobs(); + ResourcesPlugin.getWorkspace().removeResourceChangeListener(this); + super.dispose(); + } + + private class ContentProvider extends TreeNodeContentProvider { + private Map<Object, Object[]> mChildren; + private Map<IMarker, Integer> mTypeCount; + private IMarker[] mTopLevels; + + @Override + public Object[] getElements(Object inputElement) { + if (inputElement == null) { + mTypeCount = null; + return new IMarker[0]; + } + + @SuppressWarnings("unchecked") + List<IMarker> list = (List<IMarker>) inputElement; + + // Partition the children such that at the top level we have one + // marker of each type, and below we have all the duplicates of + // each one of those errors. And for errors with multiple locations, + // there is a third level. + Multimap<String, IMarker> types = ArrayListMultimap.<String, IMarker>create(100, 20); + for (IMarker marker : list) { + String id = EclipseLintClient.getId(marker); + types.put(id, marker); + } + + Set<String> ids = types.keySet(); + + mChildren = new HashMap<Object, Object[]>(ids.size()); + mTypeCount = new HashMap<IMarker, Integer>(ids.size()); + + List<IMarker> topLevel = new ArrayList<IMarker>(ids.size()); + for (String id : ids) { + Collection<IMarker> markers = types.get(id); + int childCount = markers.size(); + + // Must sort the list items in order to have a stable first item + // (otherwise preserving expanded paths etc won't work) + TableComparator sorter = getTableSorter(); + IMarker[] array = markers.toArray(new IMarker[markers.size()]); + sorter.sort(mTreeViewer, array); + + IMarker topMarker = array[0]; + mTypeCount.put(topMarker, childCount); + topLevel.add(topMarker); + + IMarker[] children = Arrays.copyOfRange(array, 1, array.length); + mChildren.put(topMarker, children); + } + + mTopLevels = topLevel.toArray(new IMarker[topLevel.size()]); + return mTopLevels; + } + + @Override + public boolean hasChildren(Object element) { + Object[] children = mChildren != null ? mChildren.get(element) : null; + return children != null && children.length > 0; + } + + @Override + public Object[] getChildren(Object parentElement) { + Object[] children = mChildren.get(parentElement); + if (children != null) { + return children; + } + + return new Object[0]; + } + + @Override + public Object getParent(Object element) { + return null; + } + + public int getCount(IMarker marker) { + if (mTypeCount != null) { + Integer count = mTypeCount.get(marker); + if (count != null) { + return count.intValue(); + } + } + + return -1; + } + + IMarker[] getTopMarkers() { + return mTopLevels; + } + } + + private class LintColumnLabelProvider extends StyledCellLabelProvider { + private LintColumn mColumn; + + LintColumnLabelProvider(LintColumn column) { + mColumn = column; + } + + @Override + public void update(ViewerCell cell) { + Object element = cell.getElement(); + cell.setImage(mColumn.getImage((IMarker) element)); + StyledString styledString = mColumn.getStyledValue((IMarker) element); + if (styledString == null) { + cell.setText(mColumn.getValue((IMarker) element)); + cell.setStyleRanges(null); + } else { + cell.setText(styledString.toString()); + cell.setStyleRanges(styledString.getStyleRanges()); + } + super.update(cell); + } + } + + TreeViewer getTreeViewer() { + return mTreeViewer; + } + + Tree getTree() { + return mTree; + } + + // ---- Implements IResourceChangeListener ---- + + @Override + public void resourceChanged(IResourceChangeEvent event) { + if (mResources == null) { + return; + } + IMarkerDelta[] deltas = event.findMarkerDeltas(AdtConstants.MARKER_LINT, true); + if (deltas.length > 0) { + // Update immediately for POST_BUILD events, otherwise do an unconditional + // update after 30 seconds. This matches the logic in Eclipse's ProblemView + // (see the MarkerView class). + if (event.getType() == IResourceChangeEvent.POST_BUILD) { + cancelJobs(); + getProgressService().schedule(mUpdateMarkersJob, 100); + } else { + IWorkbenchSiteProgressService progressService = getProgressService(); + if (progressService == null) { + mUpdateMarkersJob.schedule(30000); + } else { + getProgressService().schedule(mUpdateMarkersJob, 30000); + } + } + } + } + + // ---- Implements ControlListener ---- + + @Override + public void controlMoved(ControlEvent e) { + } + + @Override + public void controlResized(ControlEvent e) { + updateColumnWidths(); + } + + // ---- Updating Markers ---- + + private void cancelJobs() { + mUpdateMarkersJob.cancel(); + } + + protected IWorkbenchSiteProgressService getProgressService() { + if (mSite != null) { + Object siteService = mSite.getAdapter(IWorkbenchSiteProgressService.class); + if (siteService != null) { + return (IWorkbenchSiteProgressService) siteService; + } + } + return null; + } + + private class UpdateMarkersJob extends WorkbenchJob { + UpdateMarkersJob() { + super("Updating Lint Markers"); + setSystem(true); + } + + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + if (mTree.isDisposed()) { + return Status.CANCEL_STATUS; + } + + mTreeViewer.setInput(null); + List<IMarker> markerList = getMarkers(); + if (markerList.size() == 0) { + LayoutEditorDelegate delegate = + LayoutEditorDelegate.fromEditor(AdtUtils.getActiveEditor()); + if (delegate != null) { + GraphicalEditorPart g = delegate.getGraphicalEditor(); + assert g != null; + LayoutActionBar bar = g == null ? null : g.getLayoutActionBar(); + assert bar != null; + if (bar != null) { + bar.updateErrorIndicator(); + } + } + } + // Trigger selection update + Event updateEvent = new Event(); + updateEvent.widget = mTree; + mTree.notifyListeners(SWT.Selection, updateEvent); + mTreeViewer.setInput(markerList); + mTreeViewer.refresh(); + + if (mExpandedIds != null) { + List<IMarker> expanded = new ArrayList<IMarker>(mExpandedIds.size()); + IMarker[] topMarkers = mContentProvider.getTopMarkers(); + if (topMarkers != null) { + for (IMarker marker : topMarkers) { + String id = EclipseLintClient.getId(marker); + if (id != null && mExpandedIds.contains(id)) { + expanded.add(marker); + } + } + } + if (!expanded.isEmpty()) { + mTreeViewer.setExpandedElements(expanded.toArray()); + } + } + + if (mSelectedId != null) { + IMarker[] topMarkers = mContentProvider.getTopMarkers(); + for (IMarker marker : topMarkers) { + if (mSelectedId.equals(EclipseLintClient.getId(marker))) { + mTreeViewer.setSelection(new StructuredSelection(marker), true /*reveal*/); + break; + } + } + } + + return Status.OK_STATUS; + } + + @Override + public boolean shouldRun() { + // Do not run if the change came in before there is a viewer + return PlatformUI.isWorkbenchRunning(); + } + + @Override + public boolean belongsTo(Object family) { + return UPDATE_MARKERS_FAMILY == family; + } + } + + /** + * Returns the list of resources being shown in the list + * + * @return the list of resources being shown in this composite + */ + public List<? extends IResource> getResources() { + return mResources; + } + + /** Expands all nodes */ + public void expandAll() { + mTreeViewer.expandAll(); + + if (mExpandedIds == null) { + mExpandedIds = new HashSet<String>(); + } + IMarker[] topMarkers = mContentProvider.getTopMarkers(); + if (topMarkers != null) { + for (IMarker marker : topMarkers) { + String id = EclipseLintClient.getId(marker); + if (id != null) { + mExpandedIds.add(id); + } + } + } + } + + /** Collapses all nodes */ + public void collapseAll() { + mTreeViewer.collapseAll(); + mExpandedIds = null; + } + + // ---- Column Persistence ---- + + public void saveState(IMemento memento) { + if (mSingleFile) { + // Don't use persistence for single-file lists: this is a special mode of the + // window where we show a hardcoded set of columns for a single file, deliberately + // omitting the location column etc + return; + } + + IMemento columnEntry = memento.createChild(KEY_WIDTHS); + LintColumn[] columns = new LintColumn[mTree.getColumnCount()]; + int[] positions = mTree.getColumnOrder(); + for (int i = 0; i < columns.length; i++) { + TreeColumn treeColumn = mTree.getColumn(i); + LintColumn column = (LintColumn) treeColumn.getData(KEY_COLUMN); + // Workaround for TeeColumn.getWidth() returning 0 in some cases, + // see https://bugs.eclipse.org/341865 for details. + int width = getColumnWidth(column, mTreePainted); + columnEntry.putInteger(getKey(treeColumn), width); + columns[positions[i]] = column; + } + + if (getVisibleColumns() != null) { + IMemento visibleEntry = memento.createChild(KEY_VISIBLE); + for (LintColumn column : getVisibleColumns()) { + visibleEntry.putBoolean(getKey(column), true); + } + } + } + + private void createColumns() { + LintColumn[] columns = getVisibleColumns(); + TableLayout layout = new TableLayout(); + + for (int i = 0; i < columns.length; i++) { + LintColumn column = columns[i]; + TreeViewerColumn viewerColumn = null; + TreeColumn treeColumn; + viewerColumn = new TreeViewerColumn(mTreeViewer, SWT.NONE); + treeColumn = viewerColumn.getColumn(); + treeColumn.setData(KEY_COLUMN, column); + treeColumn.setResizable(true); + treeColumn.addSelectionListener(getHeaderListener()); + if (!column.isLeftAligned()) { + treeColumn.setAlignment(SWT.RIGHT); + } + viewerColumn.setLabelProvider(new LintColumnLabelProvider(column)); + treeColumn.setText(column.getColumnHeaderText()); + treeColumn.setImage(column.getColumnHeaderImage()); + IMemento columnWidths = null; + if (mMemento != null && !mSingleFile) { + columnWidths = mMemento.getChild(KEY_WIDTHS); + } + int columnWidth = getColumnWidth(column, false); + if (columnWidths != null) { + columnWidths.putInteger(getKey(column), columnWidth); + } + if (i == 0) { + // The first column should use layout -weights- to get all the + // remaining room + layout.addColumnData(new ColumnWeightData(1, true)); + } else if (columnWidth < 0) { + int defaultColumnWidth = column.getPreferredWidth(); + layout.addColumnData(new ColumnPixelData(defaultColumnWidth, true, true)); + } else { + layout.addColumnData(new ColumnPixelData(columnWidth, true)); + } + } + mTreeViewer.getTree().setLayout(layout); + mTree.layout(true); + } + + private int getColumnWidth(LintColumn column, boolean getFromUi) { + Tree tree = mTreeViewer.getTree(); + if (getFromUi) { + TreeColumn[] columns = tree.getColumns(); + for (int i = 0; i < columns.length; i++) { + if (column.equals(columns[i].getData(KEY_COLUMN))) { + return columns[i].getWidth(); + } + } + } + int preferredWidth = -1; + if (mMemento != null && !mSingleFile) { + IMemento columnWidths = mMemento.getChild(KEY_WIDTHS); + if (columnWidths != null) { + Integer value = columnWidths.getInteger(getKey(column)); + // Make sure we get a useful value + if (value != null && value.intValue() >= 0) + preferredWidth = value.intValue(); + } + } + if (preferredWidth <= 0) { + preferredWidth = Math.max(column.getPreferredWidth(), 30); + } + return preferredWidth; + } + + private static String getKey(TreeColumn treeColumn) { + return getKey((LintColumn) treeColumn.getData(KEY_COLUMN)); + } + + private static String getKey(LintColumn column) { + return column.getClass().getSimpleName(); + } + + private LintColumn[] getVisibleColumns() { + if (mVisibleColumns == null) { + if (mSingleFile) { + // Special mode where we show just lint warnings for a single file: + // use a hardcoded list of columns, not including path/location etc but + // including line numbers (which are normally not shown by default). + mVisibleColumns = new LintColumn[] { + mMessageColumn, mLineColumn + }; + } else { + // Generate visible columns based on (a) previously saved window state, + // and (b) default window visible states provided by the columns themselves + List<LintColumn> list = new ArrayList<LintColumn>(); + IMemento visibleColumns = null; + if (mMemento != null) { + visibleColumns = mMemento.getChild(KEY_VISIBLE); + } + for (LintColumn column : mColumns) { + if (visibleColumns != null) { + Boolean b = visibleColumns.getBoolean(getKey(column)); + if (b != null && b.booleanValue()) { + list.add(column); + } + } else if (column.visibleByDefault()) { + list.add(column); + } + } + if (!list.contains(mMessageColumn)) { + list.add(0, mMessageColumn); + } + mVisibleColumns = list.toArray(new LintColumn[list.size()]); + } + } + + return mVisibleColumns; + } + + int getCount(IMarker marker) { + return mContentProvider.getCount(marker); + } + + Issue getIssue(String id) { + return mRegistry.getIssue(id); + } + + Issue getIssue(IMarker marker) { + String id = EclipseLintClient.getId(marker); + return mRegistry.getIssue(id); + } + + Severity getSeverity(Issue issue) { + return mConfiguration.getSeverity(issue); + } + + // ---- Choosing visible columns ---- + + public void configureColumns() { + ColumnDialog dialog = new ColumnDialog(getShell(), mColumns, getVisibleColumns()); + if (dialog.open() == Window.OK) { + mVisibleColumns = dialog.getSelectedColumns(); + // Clear out columns: Must recreate to set the right label provider etc + for (TreeColumn column : mTree.getColumns()) { + column.dispose(); + } + createColumns(); + mTreeViewer.setComparator(new TableComparator()); + setSortIndicators(); + mTreeViewer.refresh(); + } + } + + // ---- Table Sorting ---- + + private SelectionListener getHeaderListener() { + return new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + final TreeColumn treeColumn = (TreeColumn) e.widget; + final LintColumn column = (LintColumn) treeColumn.getData(KEY_COLUMN); + + try { + IWorkbenchSiteProgressService progressService = getProgressService(); + if (progressService == null) { + BusyIndicator.showWhile(getShell().getDisplay(), new Runnable() { + @Override + public void run() { + resortTable(treeColumn, column, + new NullProgressMonitor()); + } + }); + } else { + getProgressService().busyCursorWhile(new IRunnableWithProgress() { + @Override + public void run(IProgressMonitor monitor) { + resortTable(treeColumn, column, monitor); + } + }); + } + } catch (InvocationTargetException e1) { + AdtPlugin.log(e1, null); + } catch (InterruptedException e1) { + return; + } + } + + private void resortTable(final TreeColumn treeColumn, LintColumn column, + IProgressMonitor monitor) { + TableComparator sorter = getTableSorter(); + monitor.beginTask("Sorting", 100); + monitor.worked(10); + if (column.equals(sorter.getTopColumn())) { + sorter.reverseTopPriority(); + } else { + sorter.setTopPriority(column); + } + monitor.worked(15); + PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + mTreeViewer.refresh(); + updateDirectionIndicator(treeColumn); + } + }); + monitor.done(); + } + }; + } + + private void setSortIndicators() { + LintColumn top = getTableSorter().getTopColumn(); + TreeColumn[] columns = mTreeViewer.getTree().getColumns(); + for (int i = 0; i < columns.length; i++) { + TreeColumn column = columns[i]; + if (column.getData(KEY_COLUMN).equals(top)) { + updateDirectionIndicator(column); + return; + } + } + } + + private void updateDirectionIndicator(TreeColumn column) { + Tree tree = mTreeViewer.getTree(); + tree.setSortColumn(column); + if (getTableSorter().isAscending()) { + tree.setSortDirection(SWT.UP); + } else { + tree.setSortDirection(SWT.DOWN); + } + } + + private TableComparator getTableSorter() { + return (TableComparator) mTreeViewer.getComparator(); + } + + /** Comparator used to sort the {@link LintList} tree. + * <p> + * This code is simplified from similar code in + * org.eclipse.ui.views.markers.internal.TableComparator + */ + private class TableComparator extends ViewerComparator { + private int[] mPriorities; + private boolean[] mDirections; + private int[] mDefaultPriorities; + private boolean[] mDefaultDirections; + + private TableComparator() { + int[] defaultPriorities = new int[mColumns.length]; + for (int i = 0; i < defaultPriorities.length; i++) { + defaultPriorities[i] = i; + } + mPriorities = defaultPriorities; + + boolean[] directions = new boolean[mColumns.length]; + for (int i = 0; i < directions.length; i++) { + directions[i] = mColumns[i].isAscending(); + } + mDirections = directions; + + mDefaultPriorities = new int[defaultPriorities.length]; + System.arraycopy(defaultPriorities, 0, this.mDefaultPriorities, 0, + defaultPriorities.length); + mDefaultDirections = new boolean[directions.length]; + System.arraycopy(directions, 0, this.mDefaultDirections, 0, directions.length); + } + + private void resetState() { + System.arraycopy(mDefaultPriorities, 0, mPriorities, 0, mPriorities.length); + System.arraycopy(mDefaultDirections, 0, mDirections, 0, mDirections.length); + } + + private void reverseTopPriority() { + mDirections[mPriorities[0]] = !mDirections[mPriorities[0]]; + } + + private void setTopPriority(LintColumn property) { + for (int i = 0; i < mColumns.length; i++) { + if (mColumns[i].equals(property)) { + setTopPriority(i); + return; + } + } + } + + private void setTopPriority(int priority) { + if (priority < 0 || priority >= mPriorities.length) { + return; + } + int index = -1; + for (int i = 0; i < mPriorities.length; i++) { + if (mPriorities[i] == priority) { + index = i; + } + } + if (index == -1) { + resetState(); + return; + } + // shift the array + for (int i = index; i > 0; i--) { + mPriorities[i] = mPriorities[i - 1]; + } + mPriorities[0] = priority; + mDirections[priority] = mDefaultDirections[priority]; + } + + private boolean isAscending() { + return mDirections[mPriorities[0]]; + } + + private int getTopPriority() { + return mPriorities[0]; + } + + private LintColumn getTopColumn() { + return mColumns[getTopPriority()]; + } + + @Override + public int compare(Viewer viewer, Object e1, Object e2) { + return compare((IMarker) e1, (IMarker) e2, 0, true); + } + + private int compare(IMarker marker1, IMarker marker2, int depth, + boolean continueSearching) { + if (depth >= mPriorities.length) { + return 0; + } + int column = mPriorities[depth]; + LintColumn property = mColumns[column]; + int result = property.compare(marker1, marker2); + if (result == 0 && continueSearching) { + return compare(marker1, marker2, depth + 1, continueSearching); + } + return result * (mDirections[column] ? 1 : -1); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintListDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintListDialog.java new file mode 100644 index 000000000..f88c3772c --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintListDialog.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.TitleAreaDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchPartSite; +import org.eclipse.ui.PlatformUI; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@SuppressWarnings("restriction") // WST DOM access +class LintListDialog extends TitleAreaDialog implements SelectionListener { + private static final String PROJECT_LOGO_LARGE = "android-64"; //$NON-NLS-1$ + private final IFile mFile; + private final IEditorPart mEditor; + private Button mFixButton; + private Button mIgnoreButton; + private Button mIgnoreAllButton; + private Button mShowButton; + private Text mDetailsText; + private Button mIgnoreTypeButton; + private LintList mList; + + LintListDialog( + @NonNull Shell parentShell, + @NonNull IFile file, + @Nullable IEditorPart editor) { + super(parentShell); + mFile = file; + mEditor = editor; + setHelpAvailable(false); + } + + @Override + protected void setShellStyle(int newShellStyle) { + // Allow resize + super.setShellStyle(newShellStyle | SWT.TITLE | SWT.MODELESS | SWT.RESIZE); + } + + @Override + public boolean close() { + mList.dispose(); + return super.close(); + } + + @Override + protected Control createContents(Composite parent) { + Control contents = super.createContents(parent); + setTitle("Lint Warnings in Layout"); + setMessage("Lint Errors found for the current layout:"); + setTitleImage(IconFactory.getInstance().getIcon(PROJECT_LOGO_LARGE)); + + return contents; + } + + @SuppressWarnings("unused") // SWT constructors have side effects, they are not unused + @Override + protected Control createDialogArea(Composite parent) { + Composite area = (Composite) super.createDialogArea(parent); + Composite container = new Composite(area, SWT.NONE); + container.setLayoutData(new GridData(GridData.FILL_BOTH)); + + container.setLayout(new GridLayout(2, false)); + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + IWorkbenchPartSite site = null; + if (page.getActivePart() != null) { + site = page.getActivePart().getSite(); + } + + mList = new LintList(site, container, null /*memento*/, true /*singleFile*/); + mList.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 6)); + + mShowButton = new Button(container, SWT.NONE); + mShowButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + mShowButton.setText("Show"); + mShowButton.setToolTipText("Opens the editor to reveal the XML with the issue"); + mShowButton.addSelectionListener(this); + + mFixButton = new Button(container, SWT.NONE); + mFixButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + mFixButton.setText("Fix"); + mFixButton.setToolTipText("Automatically corrects the problem, if possible"); + mFixButton.setEnabled(false); + mFixButton.addSelectionListener(this); + + mIgnoreButton = new Button(container, SWT.NONE); + mIgnoreButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + mIgnoreButton.setText("Suppress Issue"); + mIgnoreButton.setToolTipText("Adds a special attribute in the layout to suppress this specific warning"); + mIgnoreButton.addSelectionListener(this); + + mIgnoreAllButton = new Button(container, SWT.NONE); + mIgnoreAllButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + mIgnoreAllButton.setText("Suppress in Layout"); + mIgnoreAllButton.setEnabled(mEditor instanceof AndroidXmlEditor); + mIgnoreAllButton.setToolTipText("Adds an attribute on the root element to suppress all issues of this type in this layout"); + mIgnoreAllButton.addSelectionListener(this); + + mIgnoreTypeButton = new Button(container, SWT.NONE); + mIgnoreTypeButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + mIgnoreTypeButton.setText("Disable Issue Type"); + mIgnoreTypeButton.setToolTipText("Turns off checking for this type of error everywhere"); + mIgnoreTypeButton.addSelectionListener(this); + + new Label(container, SWT.NONE); + + mDetailsText = new Text(container, SWT.BORDER | SWT.READ_ONLY | SWT.WRAP + | SWT.V_SCROLL | SWT.MULTI); + Display display = parent.getDisplay(); + mDetailsText.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + mDetailsText.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + GridData gdText = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1); + gdText.heightHint = 80; + mDetailsText.setLayoutData(gdText); + + new Label(container, SWT.NONE); + + mList.addSelectionListener(this); + + mList.setResources(Collections.<IResource>singletonList(mFile)); + mList.selectFirst(); + if (mList.getSelectedMarkers().size() > 0) { + updateSelectionState(); + } + + return area; + } + + /** + * Create contents of the button bar. + */ + @Override + protected void createButtonsForButtonBar(Composite parent) { + createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); + } + + /** + * Return the initial size of the dialog. + */ + @Override + protected Point getInitialSize() { + return new Point(600, 400); + } + + private void selectMarker(IMarker marker) { + if (marker == null) { + mDetailsText.setText(""); //$NON-NLS-1$ + return; + } + + mDetailsText.setText(EclipseLintClient.describe(marker)); + } + + // ---- Implements SelectionListener ---- + + @Override + public void widgetSelected(SelectionEvent e) { + Object source = e.getSource(); + if (source == mList.getTreeViewer().getControl()) { + // Enable/disable buttons + updateSelectionState(); + } else if (source == mShowButton) { + List<IMarker> selection = mList.getSelectedMarkers(); + if (selection.size() > 0) { + EclipseLintClient.showMarker(selection.get(0)); + } + } else if (source == mFixButton) { + List<IMarker> selection = mList.getSelectedMarkers(); + for (IMarker marker : selection) { + List<LintFix> fixes = LintFix.getFixes(EclipseLintClient.getId(marker), marker); + if (fixes == null) { + continue; + } + LintFix fix = fixes.get(0); + IEditorPart editor = AdtUtils.getActiveEditor(); + if (editor instanceof AndroidXmlEditor) { + IStructuredDocument doc = ((AndroidXmlEditor) editor).getStructuredDocument(); + fix.apply(doc); + if (fix.needsFocus()) { + close(); + } + } else { + AdtPlugin.log(IStatus.ERROR, "Did not find associated editor to apply fix"); + } + } + } else if (source == mIgnoreTypeButton) { + for (IMarker marker : mList.getSelectedMarkers()) { + String id = EclipseLintClient.getId(marker); + if (id != null) { + LintFixGenerator.suppressDetector(id, true, mFile, true /*all*/); + } + } + } else if (source == mIgnoreButton) { + for (IMarker marker : mList.getSelectedMarkers()) { + LintFixGenerator.addSuppressAnnotation(marker); + } + } else if (source == mIgnoreAllButton) { + Set<String> ids = new HashSet<String>(); + for (IMarker marker : mList.getSelectedMarkers()) { + String id = EclipseLintClient.getId(marker); + if (id != null && !ids.contains(id)) { + ids.add(id); + if (mEditor instanceof AndroidXmlEditor) { + AndroidXmlEditor editor = (AndroidXmlEditor) mEditor; + AddSuppressAttribute fix = AddSuppressAttribute.createFixForAll(editor, + marker, id); + if (fix != null) { + IStructuredDocument document = editor.getStructuredDocument(); + fix.apply(document); + } + } + } + } + mList.refresh(); + } + } + + private void updateSelectionState() { + List<IMarker> selection = mList.getSelectedMarkers(); + + if (selection.size() == 1) { + selectMarker(selection.get(0)); + } else { + selectMarker(null); + } + + boolean canFix = selection.size() > 0; + for (IMarker marker : selection) { + if (!LintFix.hasFix(EclipseLintClient.getId(marker))) { + canFix = false; + break; + } + + // Some fixes cannot be run in bulk + if (selection.size() > 1) { + List<LintFix> fixes = LintFix.getFixes(EclipseLintClient.getId(marker), marker); + if (fixes == null || !fixes.get(0).isBulkCapable()) { + canFix = false; + break; + } + } + } + + mFixButton.setEnabled(canFix); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + Object source = e.getSource(); + if (source == mList.getTreeViewer().getControl()) { + // Jump to editor + List<IMarker> selection = mList.getSelectedMarkers(); + if (selection.size() > 0) { + EclipseLintClient.showMarker(selection.get(0)); + close(); + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintViewPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintViewPart.java new file mode 100644 index 000000000..90b956e32 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintViewPart.java @@ -0,0 +1,657 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import static com.android.SdkConstants.DOT_JAVA; +import static com.android.SdkConstants.DOT_XML; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.preferences.LintPreferencePage; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.tools.lint.detector.api.LintUtils; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.jobs.IJobChangeEvent; +import org.eclipse.core.runtime.jobs.IJobChangeListener; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IStatusLineManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.preference.IPreferenceNode; +import org.eclipse.jface.preference.PreferenceDialog; +import org.eclipse.jface.preference.PreferenceManager; +import org.eclipse.jface.preference.PreferenceNode; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Region; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IMemento; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.IViewPart; +import org.eclipse.ui.IViewSite; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.editors.text.TextFileDocumentProvider; +import org.eclipse.ui.part.ViewPart; +import org.eclipse.ui.texteditor.IDocumentProvider; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Eclipse View which shows lint warnings for the current project + */ +public class LintViewPart extends ViewPart implements SelectionListener, IJobChangeListener { + /** The view id for this view part */ + public static final String ID = "com.android.ide.eclipse.adt.internal.lint.LintViewPart"; //$NON-NLS-1$ + private static final String QUICKFIX_DISABLED_ICON = "quickfix-disabled"; //$NON-NLS-1$ + private static final String QUICKFIX_ICON = "quickfix"; //$NON-NLS-1$ + private static final String REFRESH_ICON = "refresh"; //$NON-NLS-1$ + private static final String EXPAND_DISABLED_ICON = "expandall-disabled"; //$NON-NLS-1$ + private static final String EXPAND_ICON = "expandall"; //$NON-NLS-1$ + private static final String COLUMNS_ICON = "columns"; //$NON-NLS-1$ + private static final String OPTIONS_ICON = "options"; //$NON-NLS-1$ + private static final String IGNORE_THIS_ICON = "ignore-this"; //$NON-NLS-1$ + private static final String IGNORE_THIS_DISABLED_ICON = "ignore-this-disabled"; //$NON-NLS-1$ + private static final String IGNORE_FILE_ICON = "ignore-file"; //$NON-NLS-1$ + private static final String IGNORE_FILE_DISABLED_ICON = "ignore-file-disabled"; //$NON-NLS-1$ + private static final String IGNORE_PRJ_ICON = "ignore-project"; //$NON-NLS-1$ + private static final String IGNORE_PRJ_DISABLED_ICON = "ignore-project-disabled"; //$NON-NLS-1$ + private static final String IGNORE_ALL_ICON = "ignore-all"; //$NON-NLS-1$ + private static final String IGNORE_ALL_DISABLED_ICON = "ignore-all-disabled"; //$NON-NLS-1$ + private IMemento mMemento; + private LintList mLintView; + private Text mDetailsText; + private Label mErrorLabel; + private SashForm mSashForm; + private Action mFixAction; + private Action mRemoveAction; + private Action mIgnoreAction; + private Action mAlwaysIgnoreAction; + private Action mIgnoreFileAction; + private Action mIgnoreProjectAction; + private Action mRemoveAllAction; + private Action mRefreshAction; + private Action mExpandAll; + private Action mCollapseAll; + private Action mConfigureColumns; + private Action mOptions; + + /** + * Initial projects to show: this field is only briefly not null during the + * construction initiated by {@link #show(List)} + */ + private static List<? extends IResource> sInitialResources; + + /** + * Constructs a new {@link LintViewPart} + */ + public LintViewPart() { + } + + @Override + public void init(IViewSite site, IMemento memento) throws PartInitException { + super.init(site, memento); + mMemento = memento; + } + + @Override + public void saveState(IMemento memento) { + super.saveState(memento); + + mLintView.saveState(memento); + } + + @Override + public void dispose() { + if (mLintView != null) { + mLintView.dispose(); + mLintView = null; + } + super.dispose(); + } + + @Override + public void createPartControl(Composite parent) { + GridLayout gridLayout = new GridLayout(1, false); + gridLayout.verticalSpacing = 0; + gridLayout.marginWidth = 0; + gridLayout.marginHeight = 0; + parent.setLayout(gridLayout); + + mErrorLabel = new Label(parent, SWT.NONE); + mErrorLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + + mSashForm = new SashForm(parent, SWT.NONE); + mSashForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); + mLintView = new LintList(getSite(), mSashForm, mMemento, false /*singleFile*/); + + mDetailsText = new Text(mSashForm, + SWT.BORDER | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL | SWT.MULTI); + Display display = parent.getDisplay(); + mDetailsText.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + mDetailsText.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + + mLintView.addSelectionListener(this); + mSashForm.setWeights(new int[] {8, 2}); + + createActions(); + initializeToolBar(); + + // If there are currently running jobs, listen for them such that we can update the + // button state + refreshStopIcon(); + + if (sInitialResources != null) { + mLintView.setResources(sInitialResources); + sInitialResources = null; + } else { + // No supplied context: show lint warnings for all projects + IJavaProject[] androidProjects = BaseProjectHelper.getAndroidProjects(null); + if (androidProjects.length > 0) { + List<IResource> projects = new ArrayList<IResource>(); + for (IJavaProject project : androidProjects) { + projects.add(project.getProject()); + } + mLintView.setResources(projects); + } + } + + updateIssueCount(); + } + + /** + * Create the actions. + */ + private void createActions() { + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + IconFactory iconFactory = IconFactory.getInstance(); + mFixAction = new LintViewAction("Fix", ACTION_FIX, + iconFactory.getImageDescriptor(QUICKFIX_ICON), + iconFactory.getImageDescriptor(QUICKFIX_DISABLED_ICON)); + + mIgnoreAction = new LintViewAction("Suppress this error with an annotation/attribute", + ACTION_IGNORE_THIS, + iconFactory.getImageDescriptor(IGNORE_THIS_ICON), + iconFactory.getImageDescriptor(IGNORE_THIS_DISABLED_ICON)); + mIgnoreFileAction = new LintViewAction("Ignore in this file", ACTION_IGNORE_FILE, + iconFactory.getImageDescriptor(IGNORE_FILE_ICON), + iconFactory.getImageDescriptor(IGNORE_FILE_DISABLED_ICON)); + mIgnoreProjectAction = new LintViewAction("Ignore in this project", ACTION_IGNORE_TYPE, + iconFactory.getImageDescriptor(IGNORE_PRJ_ICON), + iconFactory.getImageDescriptor(IGNORE_PRJ_DISABLED_ICON)); + mAlwaysIgnoreAction = new LintViewAction("Always Ignore", ACTION_IGNORE_ALL, + iconFactory.getImageDescriptor(IGNORE_ALL_ICON), + iconFactory.getImageDescriptor(IGNORE_ALL_DISABLED_ICON)); + + mRemoveAction = new LintViewAction("Remove", ACTION_REMOVE, + sharedImages.getImageDescriptor(ISharedImages.IMG_ELCL_REMOVE), + sharedImages.getImageDescriptor(ISharedImages.IMG_ELCL_REMOVE_DISABLED)); + mRemoveAllAction = new LintViewAction("Remove All", ACTION_REMOVE_ALL, + sharedImages.getImageDescriptor(ISharedImages.IMG_ELCL_REMOVEALL), + sharedImages.getImageDescriptor(ISharedImages.IMG_ELCL_REMOVEALL_DISABLED)); + mRefreshAction = new LintViewAction("Refresh (& Save Files)", ACTION_REFRESH, + iconFactory.getImageDescriptor(REFRESH_ICON), null); + mRemoveAllAction.setEnabled(true); + mCollapseAll = new LintViewAction("Collapse All", ACTION_COLLAPSE, + sharedImages.getImageDescriptor(ISharedImages.IMG_ELCL_COLLAPSEALL), + sharedImages.getImageDescriptor(ISharedImages.IMG_ELCL_COLLAPSEALL_DISABLED)); + mCollapseAll.setEnabled(true); + mExpandAll = new LintViewAction("Expand All", ACTION_EXPAND, + iconFactory.getImageDescriptor(EXPAND_ICON), + iconFactory.getImageDescriptor(EXPAND_DISABLED_ICON)); + mExpandAll.setEnabled(true); + + mConfigureColumns = new LintViewAction("Configure Columns...", ACTION_COLUMNS, + iconFactory.getImageDescriptor(COLUMNS_ICON), + null); + + mOptions = new LintViewAction("Options...", ACTION_OPTIONS, + iconFactory.getImageDescriptor(OPTIONS_ICON), + null); + + enableActions(Collections.<IMarker>emptyList(), false /*updateWidgets*/); + } + + /** + * Initialize the toolbar. + */ + private void initializeToolBar() { + IToolBarManager toolbarManager = getViewSite().getActionBars().getToolBarManager(); + toolbarManager.add(mRefreshAction); + toolbarManager.add(mFixAction); + toolbarManager.add(mIgnoreAction); + toolbarManager.add(mIgnoreFileAction); + toolbarManager.add(mIgnoreProjectAction); + toolbarManager.add(mAlwaysIgnoreAction); + toolbarManager.add(new Separator()); + toolbarManager.add(mRemoveAction); + toolbarManager.add(mRemoveAllAction); + toolbarManager.add(new Separator()); + toolbarManager.add(mExpandAll); + toolbarManager.add(mCollapseAll); + toolbarManager.add(mConfigureColumns); + toolbarManager.add(mOptions); + } + + @Override + public void setFocus() { + mLintView.setFocus(); + } + + /** + * Sets the resource associated with the lint view + * + * @param resources the associated resources + */ + public void setResources(List<? extends IResource> resources) { + mLintView.setResources(resources); + + // Refresh the stop/refresh icon status + refreshStopIcon(); + } + + private void refreshStopIcon() { + Job[] currentJobs = LintJob.getCurrentJobs(); + if (currentJobs.length > 0) { + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + mRefreshAction.setImageDescriptor(sharedImages.getImageDescriptor( + ISharedImages.IMG_ELCL_STOP)); + for (Job job : currentJobs) { + job.addJobChangeListener(this); + } + } else { + mRefreshAction.setImageDescriptor( + IconFactory.getInstance().getImageDescriptor(REFRESH_ICON)); + + } + } + + // ---- Implements SelectionListener ---- + + @Override + public void widgetSelected(SelectionEvent e) { + List<IMarker> markers = mLintView.getSelectedMarkers(); + if (markers.size() != 1) { + mDetailsText.setText(""); //$NON-NLS-1$ + } else { + mDetailsText.setText(EclipseLintClient.describe(markers.get(0))); + } + + IStatusLineManager status = getViewSite().getActionBars().getStatusLineManager(); + status.setMessage(mDetailsText.getText()); + + updateIssueCount(); + + enableActions(markers, true /* updateWidgets */); + } + + private void enableActions(List<IMarker> markers, boolean updateWidgets) { + // Update enabled state of actions + boolean hasSelection = markers.size() > 0; + boolean canFix = hasSelection; + for (IMarker marker : markers) { + if (!LintFix.hasFix(EclipseLintClient.getId(marker))) { + canFix = false; + break; + } + + // Some fixes cannot be run in bulk + if (markers.size() > 1) { + List<LintFix> fixes = LintFix.getFixes(EclipseLintClient.getId(marker), marker); + if (fixes == null || !fixes.get(0).isBulkCapable()) { + canFix = false; + break; + } + } + } + + boolean haveFile = false; + boolean isJavaOrXml = true; + for (IMarker marker : markers) { + IResource resource = marker.getResource(); + if (resource instanceof IFile || resource instanceof IFolder) { + haveFile = true; + String name = resource.getName(); + if (!LintUtils.endsWith(name, DOT_XML) && !LintUtils.endsWith(name, DOT_JAVA)) { + isJavaOrXml = false; + } + break; + } + } + + mFixAction.setEnabled(canFix); + mIgnoreAction.setEnabled(hasSelection && haveFile && isJavaOrXml); + mIgnoreFileAction.setEnabled(hasSelection && haveFile); + mIgnoreProjectAction.setEnabled(hasSelection); + mAlwaysIgnoreAction.setEnabled(hasSelection); + mRemoveAction.setEnabled(hasSelection); + + if (updateWidgets) { + getViewSite().getActionBars().getToolBarManager().update(false); + } + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + Object source = e.getSource(); + if (source == mLintView.getTreeViewer().getControl()) { + // Jump to editor + List<IMarker> selection = mLintView.getSelectedMarkers(); + if (selection.size() > 0) { + EclipseLintClient.showMarker(selection.get(0)); + } + } + } + + // --- Implements IJobChangeListener ---- + + @Override + public void done(IJobChangeEvent event) { + mRefreshAction.setImageDescriptor( + IconFactory.getInstance().getImageDescriptor(REFRESH_ICON)); + + if (!mLintView.isDisposed()) { + mLintView.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + if (!mLintView.isDisposed()) { + updateIssueCount(); + } + } + }); + } + } + + private void updateIssueCount() { + int errors = mLintView.getErrorCount(); + int warnings = mLintView.getWarningCount(); + mErrorLabel.setText(String.format("%1$d errors, %2$d warnings", errors, warnings)); + } + + @Override + public void aboutToRun(IJobChangeEvent event) { + } + + @Override + public void awake(IJobChangeEvent event) { + } + + @Override + public void running(IJobChangeEvent event) { + } + + @Override + public void scheduled(IJobChangeEvent event) { + } + + @Override + public void sleeping(IJobChangeEvent event) { + } + + // ---- Actions ---- + + private static final int ACTION_REFRESH = 1; + private static final int ACTION_FIX = 2; + private static final int ACTION_IGNORE_THIS = 3; + private static final int ACTION_IGNORE_FILE = 4; + private static final int ACTION_IGNORE_TYPE = 5; + private static final int ACTION_IGNORE_ALL = 6; + private static final int ACTION_REMOVE = 7; + private static final int ACTION_REMOVE_ALL = 8; + private static final int ACTION_COLLAPSE = 9; + private static final int ACTION_EXPAND = 10; + private static final int ACTION_COLUMNS = 11; + private static final int ACTION_OPTIONS = 12; + + private class LintViewAction extends Action { + + private final int mAction; + + private LintViewAction(String label, int action, + ImageDescriptor imageDesc, ImageDescriptor disabledImageDesc) { + super(label); + mAction = action; + setImageDescriptor(imageDesc); + if (disabledImageDesc != null) { + setDisabledImageDescriptor(disabledImageDesc); + } + } + + @Override + public void run() { + switch (mAction) { + case ACTION_REFRESH: { + IWorkbench workbench = PlatformUI.getWorkbench(); + if (workbench != null) { + workbench.saveAllEditors(false /*confirm*/); + } + + Job[] jobs = LintJob.getCurrentJobs(); + if (jobs.length > 0) { + EclipseLintRunner.cancelCurrentJobs(false); + } else { + List<? extends IResource> resources = mLintView.getResources(); + if (resources == null) { + return; + } + Job job = EclipseLintRunner.startLint(resources, null, null, + false /*fatalOnly*/, false /*show*/); + if (job != null && workbench != null) { + job.addJobChangeListener(LintViewPart.this); + ISharedImages sharedImages = workbench.getSharedImages(); + setImageDescriptor(sharedImages.getImageDescriptor( + ISharedImages.IMG_ELCL_STOP)); + } + } + break; + } + case ACTION_FIX: { + List<IMarker> markers = mLintView.getSelectedMarkers(); + for (IMarker marker : markers) { + List<LintFix> fixes = LintFix.getFixes(EclipseLintClient.getId(marker), + marker); + if (fixes == null) { + continue; + } + LintFix fix = fixes.get(0); + IResource resource = marker.getResource(); + if (fix.needsFocus() && resource instanceof IFile) { + IRegion region = null; + try { + int start = marker.getAttribute(IMarker.CHAR_START, -1); + int end = marker.getAttribute(IMarker.CHAR_END, -1); + if (start != -1) { + region = new Region(start, end - start); + } + AdtPlugin.openFile((IFile) resource, region); + } catch (PartInitException e) { + AdtPlugin.log(e, "Can't open file %1$s", resource); + } + } + IDocumentProvider provider = new TextFileDocumentProvider(); + try { + provider.connect(resource); + IDocument document = provider.getDocument(resource); + if (document != null) { + fix.apply(document); + if (!fix.needsFocus()) { + provider.saveDocument(new NullProgressMonitor(), resource, + document, true /*overwrite*/); + } + } + } catch (Exception e) { + AdtPlugin.log(e, "Did not find associated editor to apply fix: %1$s", + resource.getName()); + } finally { + provider.disconnect(resource); + } + } + break; + } + case ACTION_REMOVE: { + for (IMarker marker : mLintView.getSelectedMarkers()) { + try { + marker.delete(); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + break; + } + case ACTION_REMOVE_ALL: { + List<? extends IResource> resources = mLintView.getResources(); + if (resources != null) { + for (IResource resource : resources) { + EclipseLintClient.clearMarkers(resource); + } + } + break; + } + case ACTION_IGNORE_ALL: + assert false; + break; + case ACTION_IGNORE_TYPE: + case ACTION_IGNORE_FILE: { + boolean ignoreInFile = mAction == ACTION_IGNORE_FILE; + for (IMarker marker : mLintView.getSelectedMarkers()) { + String id = EclipseLintClient.getId(marker); + if (id != null) { + IResource resource = marker.getResource(); + LintFixGenerator.suppressDetector(id, true, + ignoreInFile ? resource : resource.getProject(), + ignoreInFile); + } + } + break; + } + case ACTION_IGNORE_THIS: { + for (IMarker marker : mLintView.getSelectedMarkers()) { + LintFixGenerator.addSuppressAnnotation(marker); + } + break; + } + case ACTION_COLLAPSE: { + mLintView.collapseAll(); + break; + } + case ACTION_EXPAND: { + mLintView.expandAll(); + break; + } + case ACTION_COLUMNS: { + mLintView.configureColumns(); + break; + } + case ACTION_OPTIONS: { + PreferenceManager manager = new PreferenceManager(); + + LintPreferencePage page = new LintPreferencePage(); + String title = "Default/Global Settings"; + page.setTitle(title); + IPreferenceNode node = new PreferenceNode(title, page); + manager.addToRoot(node); + + + List<? extends IResource> resources = mLintView.getResources(); + if (resources != null) { + Set<IProject> projects = new HashSet<IProject>(); + for (IResource resource : resources) { + projects.add(resource.getProject()); + } + if (projects.size() > 0) { + for (IProject project : projects) { + page = new LintPreferencePage(); + page.setTitle(String.format("Settings for %1$s", + project.getName())); + page.setElement(project); + node = new PreferenceNode(project.getName(), page); + manager.addToRoot(node); + } + } + } + + Shell shell = LintViewPart.this.getSite().getShell(); + PreferenceDialog dialog = new PreferenceDialog(shell, manager); + dialog.create(); + dialog.setSelectedNode(title); + dialog.open(); + break; + } + default: + assert false : mAction; + } + updateIssueCount(); + } + } + + /** + * Shows or reconfigures the LintView to show the lint warnings for the + * given project + * + * @param projects the projects to show lint warnings for + */ + public static void show(List<? extends IResource> projects) { + IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (window != null) { + IWorkbenchPage page = window.getActivePage(); + if (page != null) { + try { + // Pass initial project context via static field read by constructor + sInitialResources = projects; + IViewPart view = page.showView(LintViewPart.ID, null, + IWorkbenchPage.VIEW_ACTIVATE); + if (sInitialResources != null && view instanceof LintViewPart) { + // The view must be showing already since the constructor was not + // run, so reconfigure the view instead + LintViewPart lintView = (LintViewPart) view; + lintView.setResources(projects); + } + } catch (PartInitException e) { + AdtPlugin.log(e, "Cannot open Lint View"); + } finally { + sInitialResources = null; + } + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ObsoleteLayoutParamsFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ObsoleteLayoutParamsFix.java new file mode 100644 index 000000000..9db551733 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ObsoleteLayoutParamsFix.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.text.IDocument; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +@SuppressWarnings("restriction") // DOM model +final class ObsoleteLayoutParamsFix extends DocumentFix { + private ObsoleteLayoutParamsFix(String id, IMarker marker) { + super(id, marker); + } + + @Override + public boolean needsFocus() { + return false; + } + + @Override + public boolean isCancelable() { + return false; + } + + @Override + public boolean isBulkCapable() { + return false; + } + + @Override + protected void apply(IDocument document, IStructuredModel model, Node node, int start, + int end) { + if (node instanceof Element) { + Element element = (Element) node; + NamedNodeMap attributes = element.getAttributes(); + for (int i = 0, n = attributes.getLength(); i < n; i++) { + Attr attribute = (Attr) attributes.item(i); + if (attribute instanceof IndexedRegion) { + IndexedRegion region = (IndexedRegion) attribute; + if (region.getStartOffset() == start) { + element.removeAttribute(attribute.getName()); + return; + } + } + } + } + } + + @Override + public String getDisplayString() { + return "Remove attribute"; + } + + @Override + public Image getImage() { + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + return sharedImages.getImage(ISharedImages.IMG_ETOOL_DELETE); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ProjectLintConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ProjectLintConfiguration.java new file mode 100644 index 000000000..9e4ca1226 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ProjectLintConfiguration.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import com.android.annotations.NonNull; +import com.android.annotations.VisibleForTesting; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.tools.lint.client.api.Configuration; +import com.android.tools.lint.client.api.DefaultConfiguration; +import com.android.tools.lint.client.api.LintClient; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.Project; +import com.android.tools.lint.detector.api.Severity; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.QualifiedName; + +import java.io.File; + +/** Configuration for Lint in Eclipse projects */ +class ProjectLintConfiguration extends DefaultConfiguration { + private boolean mFatalOnly; + + private final static QualifiedName CONFIGURATION_NAME = new QualifiedName(AdtPlugin.PLUGIN_ID, + "lintconfig"); //$NON-NLS-1$ + + @VisibleForTesting + ProjectLintConfiguration(LintClient client, Project project, + Configuration parent, boolean fatalOnly) { + super(client, project, parent); + mFatalOnly = fatalOnly; + } + + private static ProjectLintConfiguration create(LintClient client, IProject project, + Configuration parent, boolean fatalOnly) { + File dir = AdtUtils.getAbsolutePath(project).toFile(); + Project lintProject = client.getProject(dir, dir); + return new ProjectLintConfiguration(client, lintProject, parent, fatalOnly); + } + + public static ProjectLintConfiguration get(LintClient client, IProject project, + boolean fatalOnly) { + // Don't cache fatal-only configurations: they're only used occasionally and typically + // not repeatedly + if (fatalOnly) { + return create(client, project, GlobalLintConfiguration.get(), true); + } + + ProjectLintConfiguration configuration = null; + try { + Object value = project.getSessionProperty(CONFIGURATION_NAME); + configuration = (ProjectLintConfiguration) value; + } catch (CoreException e) { + // Not a problem; we will just create a new one + } + if (configuration == null) { + configuration = create(client, project, GlobalLintConfiguration.get(), false); + try { + project.setSessionProperty(CONFIGURATION_NAME, configuration); + } catch (CoreException e) { + AdtPlugin.log(e, "Can't store lint configuration"); + } + } + return configuration; + } + + @Override + public @NonNull Severity getSeverity(@NonNull Issue issue) { + Severity severity = super.getSeverity(issue); + if (mFatalOnly && severity != Severity.FATAL) { + return Severity.IGNORE; + } + return severity; + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/RemoveUselessViewFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/RemoveUselessViewFix.java new file mode 100644 index 000000000..0e9f326bf --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/RemoveUselessViewFix.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.UnwrapRefactoring; +import com.android.tools.lint.checks.UselessViewDetector; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextSelection; +import org.eclipse.jface.text.TextSelection; +import org.eclipse.ltk.ui.refactoring.RefactoringWizard; +import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +@SuppressWarnings("restriction") // DOM model +final class RemoveUselessViewFix extends DocumentFix { + private RemoveUselessViewFix(String id, IMarker marker) { + super(id, marker); + } + + @Override + public boolean needsFocus() { + return isCancelable(); + } + + @Override + public boolean isCancelable() { + return mId.equals(mId.equals(UselessViewDetector.USELESS_PARENT.getId())); + } + + @Override + protected void apply(IDocument document, IStructuredModel model, Node node, int start, + int end) { + if (node instanceof Element && node.getParentNode() instanceof Element) { + Element element = (Element) node; + Element parent = (Element) node.getParentNode(); + + if (mId.equals(UselessViewDetector.USELESS_LEAF.getId())) { + parent.removeChild(element); + } else { + assert mId.equals(UselessViewDetector.USELESS_PARENT.getId()); + // Invoke refactoring + LayoutEditorDelegate delegate = + LayoutEditorDelegate.fromEditor(AdtUtils.getActiveEditor()); + + if (delegate != null) { + IFile file = (IFile) mMarker.getResource(); + ITextSelection textSelection = new TextSelection(start, + end - start); + UnwrapRefactoring refactoring = + new UnwrapRefactoring(file, delegate, textSelection, null); + RefactoringWizard wizard = refactoring.createWizard(); + RefactoringWizardOpenOperation op = + new RefactoringWizardOpenOperation(wizard); + try { + IWorkbenchWindow window = PlatformUI.getWorkbench(). + getActiveWorkbenchWindow(); + op.run(window.getShell(), wizard.getDefaultPageTitle()); + } catch (InterruptedException e) { + } + } + } + } + } + + @Override + public String getDisplayString() { + return "Remove unnecessary view"; + } + + @Override + public Image getImage() { + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + return sharedImages.getImage(ISharedImages.IMG_ETOOL_DELETE); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/RunLintAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/RunLintAction.java new file mode 100644 index 000000000..1de903e23 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/RunLintAction.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.lint; + +import static com.android.SdkConstants.DOT_XML; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.tools.lint.detector.api.LintUtils; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.ui.JavaElementLabelProvider; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.ActionContributionItem; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IMenuCreator; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.ui.IObjectActionDelegate; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.IWorkbenchWindowPulldownDelegate; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.texteditor.ITextEditor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Action which runs Lint on the currently projects (and also provides a + * pulldown menu in the toolbar for selecting specifically which projects to + * check) + */ +public class RunLintAction implements IObjectActionDelegate, IMenuCreator, + IWorkbenchWindowPulldownDelegate { + + private ISelection mSelection; + private Menu mMenu; + + @Override + public void selectionChanged(IAction action, ISelection selection) { + mSelection = selection; + } + + @Override + public void run(IAction action) { + List<IProject> projects = getProjects(mSelection, true /* warn */); + + if (!projects.isEmpty()) { + EclipseLintRunner.startLint(projects, null, null, false /*fatalOnly*/, true /*show*/); + } + } + + /** Returns the Android project(s) to apply a lint run to. */ + static List<IProject> getProjects(ISelection selection, boolean warn) { + List<IProject> projects = AdtUtils.getSelectedProjects(selection); + + if (projects.isEmpty() && warn) { + MessageDialog.openWarning(AdtPlugin.getShell(), "Lint", + "Could not run Lint: Select an Android project first."); + } + + return projects; + } + + @Override + public void setActivePart(IAction action, IWorkbenchPart targetPart) { + } + + @Override + public void dispose() { + if (mMenu != null) { + mMenu.dispose(); + } + } + + @Override + public void init(IWorkbenchWindow window) { + } + + // ---- IMenuCreator ---- + + @Override + public Menu getMenu(Control parent) { + mMenu = new Menu(parent); + + IconFactory iconFactory = IconFactory.getInstance(); + ImageDescriptor allIcon = iconFactory.getImageDescriptor("lintrun"); //$NON-NLS-1$ + LintMenuAction allAction = new LintMenuAction("Check All Projects", allIcon, + ACTION_RUN, null); + + addAction(allAction); + addSeparator(); + IJavaProject[] projects = AdtUtils.getOpenAndroidProjects(); + ILabelProvider provider = new JavaElementLabelProvider( + JavaElementLabelProvider.SHOW_DEFAULT); + for (IJavaProject project : projects) { + IProject p = project.getProject(); + ImageDescriptor icon = ImageDescriptor.createFromImage(provider.getImage(p)); + String label = String.format("Check %1$s", p.getName()); + LintMenuAction projectAction = new LintMenuAction(label, icon, ACTION_RUN, p); + addAction(projectAction); + } + + ITextEditor textEditor = AdtUtils.getActiveTextEditor(); + if (textEditor != null) { + IFile file = AdtUtils.getActiveFile(); + // Currently only supported for XML files + if (file != null && LintUtils.endsWith(file.getName(), DOT_XML)) { + ImageDescriptor icon = ImageDescriptor.createFromImage(provider.getImage(file)); + IAction fileAction = new LintMenuAction("Check Current File", icon, ACTION_RUN, + file); + + addSeparator(); + addAction(fileAction); + } + } + + ISharedImages images = PlatformUI.getWorkbench().getSharedImages(); + ImageDescriptor clear = images.getImageDescriptor(ISharedImages.IMG_ELCL_REMOVEALL); + LintMenuAction clearAction = new LintMenuAction("Clear Lint Warnings", clear, ACTION_CLEAR, + null); + addSeparator(); + addAction(clearAction); + + LintMenuAction excludeAction = new LintMenuAction("Skip Library Project Dependencies", + allIcon, ACTION_TOGGLE_EXCLUDE, null); + addSeparator(); + addAction(excludeAction); + excludeAction.setChecked(AdtPrefs.getPrefs().getSkipLibrariesFromLint()); + + return mMenu; + } + + private void addAction(IAction action) { + ActionContributionItem item = new ActionContributionItem(action); + item.fill(mMenu, -1); + } + + private void addSeparator() { + new Separator().fill(mMenu, -1); + } + + @Override + public Menu getMenu(Menu parent) { + return null; + } + + private static final int ACTION_RUN = 1; + private static final int ACTION_CLEAR = 2; + private static final int ACTION_TOGGLE_EXCLUDE = 3; + + /** + * Actions in the pulldown context menu: run lint or clear lint markers on + * the given resource + */ + private static class LintMenuAction extends Action { + private final IResource mResource; + private final int mAction; + + /** + * Creates a new context menu action + * + * @param text the label + * @param descriptor the icon + * @param action the action to run: run lint, clear, or toggle exclude libraries + * @param resource the resource to check or clear markers for, where + * null means all projects + */ + private LintMenuAction(String text, ImageDescriptor descriptor, int action, + IResource resource) { + super(text, action == ACTION_TOGGLE_EXCLUDE ? AS_CHECK_BOX : AS_PUSH_BUTTON); + if (descriptor != null) { + setImageDescriptor(descriptor); + } + mAction = action; + mResource = resource; + } + + @Override + public void run() { + if (mAction == ACTION_TOGGLE_EXCLUDE) { + AdtPrefs prefs = AdtPrefs.getPrefs(); + prefs.setSkipLibrariesFromLint(!prefs.getSkipLibrariesFromLint()); + return; + } + List<IResource> resources = new ArrayList<IResource>(); + if (mResource == null) { + // All projects + IJavaProject[] open = AdtUtils.getOpenAndroidProjects(); + for (IJavaProject project : open) { + resources.add(project.getProject()); + } + } else { + resources.add(mResource); + } + EclipseLintRunner.cancelCurrentJobs(false); + if (mAction == ACTION_CLEAR) { + EclipseLintClient.clearMarkers(resources); + } else { + assert mAction == ACTION_RUN; + EclipseLintRunner.startLint(resources, null, null, false /*fatalOnly*/, + true /*show*/); + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetAttributeFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetAttributeFix.java new file mode 100644 index 000000000..ea73b9a72 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetAttributeFix.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_ALLOW_BACKUP; +import static com.android.SdkConstants.ATTR_BASELINE_ALIGNED; +import static com.android.SdkConstants.ATTR_CONTENT_DESCRIPTION; +import static com.android.SdkConstants.ATTR_ID; +import static com.android.SdkConstants.ATTR_INPUT_TYPE; +import static com.android.SdkConstants.ATTR_PERMISSION; +import static com.android.SdkConstants.ATTR_TRANSLATABLE; +import static com.android.SdkConstants.NEW_ID_PREFIX; +import static com.android.SdkConstants.VALUE_FALSE; + +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; +import com.android.tools.lint.checks.AccessibilityDetector; +import com.android.tools.lint.checks.InefficientWeightDetector; +import com.android.tools.lint.checks.ManifestDetector; +import com.android.tools.lint.checks.MissingIdDetector; +import com.android.tools.lint.checks.SecurityDetector; +import com.android.tools.lint.checks.TextFieldDetector; +import com.android.tools.lint.checks.TranslationDetector; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.ui.IEditorPart; +import org.w3c.dom.Element; + +/** Shared fix class for various builtin attributes */ +final class SetAttributeFix extends SetPropertyFix { + private SetAttributeFix(String id, IMarker marker) { + super(id, marker); + } + + @Override + protected String getAttribute() { + if (mId.equals(AccessibilityDetector.ISSUE.getId())) { + return ATTR_CONTENT_DESCRIPTION; + } else if (mId.equals(InefficientWeightDetector.BASELINE_WEIGHTS.getId())) { + return ATTR_BASELINE_ALIGNED; + } else if (mId.equals(SecurityDetector.EXPORTED_SERVICE.getId())) { + return ATTR_PERMISSION; + } else if (mId.equals(TextFieldDetector.ISSUE.getId())) { + return ATTR_INPUT_TYPE; + } else if (mId.equals(TranslationDetector.MISSING.getId())) { + return ATTR_TRANSLATABLE; + } else if (mId.equals(ManifestDetector.ALLOW_BACKUP.getId())) { + return ATTR_ALLOW_BACKUP; + } else if (mId.equals(MissingIdDetector.ISSUE.getId())) { + return ATTR_ID; + } else { + assert false : mId; + return ""; + } + } + + @Override + protected boolean isAndroidAttribute() { + if (mId.equals(TranslationDetector.MISSING.getId())) { + return false; + } + + return true; + } + + @Override + public String getDisplayString() { + if (mId.equals(AccessibilityDetector.ISSUE.getId())) { + return "Add content description attribute"; + } else if (mId.equals(InefficientWeightDetector.BASELINE_WEIGHTS.getId())) { + return "Set baseline attribute"; + } else if (mId.equals(TextFieldDetector.ISSUE.getId())) { + return "Set input type"; + } else if (mId.equals(SecurityDetector.EXPORTED_SERVICE.getId())) { + return "Add permission attribute"; + } else if (mId.equals(TranslationDetector.MISSING.getId())) { + return "Mark this as a non-translatable resource"; + } else if (mId.equals(ManifestDetector.ALLOW_BACKUP.getId())) { + return "Set the allowBackup attribute to true or false"; + } else if (mId.equals(MissingIdDetector.ISSUE.getId())) { + return "Set the ID attribute"; + } else { + assert false : mId; + return ""; + } + } + + @Override + public String getAdditionalProposalInfo() { + String help = super.getAdditionalProposalInfo(); + + if (mId.equals(TranslationDetector.MISSING.getId())) { + help = "<b>Adds translatable=\"false\" to this <string>.</b><br><br>" + help; + } + + return help; + } + + @Override + protected boolean invokeCodeCompletion() { + return mId.equals(SecurityDetector.EXPORTED_SERVICE.getId()) + || mId.equals(TextFieldDetector.ISSUE.getId()) + || mId.equals(ManifestDetector.ALLOW_BACKUP.getId()); + } + + @Override + public boolean selectValue() { + if (mId.equals(TranslationDetector.MISSING.getId())) { + return false; + } else { + return super.selectValue(); + } + } + + @Override + protected String getProposal(Element element) { + if (mId.equals(InefficientWeightDetector.BASELINE_WEIGHTS.getId())) { + return VALUE_FALSE; + } else if (mId.equals(TranslationDetector.MISSING.getId())) { + return VALUE_FALSE; + } else if (mId.equals(TextFieldDetector.ISSUE.getId())) { + return element.getAttributeNS(ANDROID_URI, ATTR_INPUT_TYPE); + } else if (mId.equals(MissingIdDetector.ISSUE.getId())) { + IEditorPart editor = AdtUtils.getActiveEditor(); + if (editor instanceof AndroidXmlEditor) { + AndroidXmlEditor xmlEditor = (AndroidXmlEditor) editor; + return DescriptorsUtils.getFreeWidgetId(xmlEditor.getUiRootNode(), + "fragment"); //$NON-NLS-1$ + } else { + return NEW_ID_PREFIX; + } + } + + return super.getProposal(element); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetPropertyFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetPropertyFix.java new file mode 100644 index 000000000..a2b79c3c8 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetPropertyFix.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import static com.android.SdkConstants.ANDROID_URI; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.utils.XmlUtils; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.Region; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +@SuppressWarnings("restriction") // DOM model +abstract class SetPropertyFix extends DocumentFix { + private Region mSelect; + + protected SetPropertyFix(String id, IMarker marker) { + super(id, marker); + } + + /** Attribute to be added */ + protected abstract String getAttribute(); + + /** Whether it's in the android: namespace */ + protected abstract boolean isAndroidAttribute(); + + protected String getProposal(Element element) { + return invokeCodeCompletion() ? "" : "TODO"; //$NON-NLS-1$ + } + + protected boolean invokeCodeCompletion() { + return false; + } + + @Override + public boolean isCancelable() { + return false; + } + + @Override + protected void apply(IDocument document, IStructuredModel model, Node node, int start, + int end) { + mSelect = null; + + if (node instanceof Element) { + Element element = (Element) node; + String proposal = getProposal(element); + String localAttribute = getAttribute(); + String prefix = null; + if (isAndroidAttribute()) { + prefix = XmlUtils.lookupNamespacePrefix(node, ANDROID_URI); + } + String attribute = prefix != null ? prefix + ':' + localAttribute : localAttribute; + + // This does not work even though it should: it does not include the prefix + //element.setAttributeNS(ANDROID_URI, localAttribute, proposal); + // So workaround instead: + element.setAttribute(attribute, proposal); + + Attr attr = null; + if (isAndroidAttribute()) { + attr = element.getAttributeNodeNS(ANDROID_URI, localAttribute); + } else { + attr = element.getAttributeNode(localAttribute); + } + if (attr instanceof IndexedRegion) { + IndexedRegion region = (IndexedRegion) attr; + int offset = region.getStartOffset(); + // We only want to select the value part inside the quotes, + // so skip the attribute and =" parts added by WST: + offset += attribute.length() + 2; + if (selectValue()) { + mSelect = new Region(offset, proposal.length()); + } + } + } + } + + protected boolean selectValue() { + return true; + } + + @Override + public void apply(IDocument document) { + try { + IFile file = (IFile) mMarker.getResource(); + super.apply(document); + AdtPlugin.openFile(file, mSelect, true); + } catch (PartInitException e) { + AdtPlugin.log(e, null); + } + + // Invoke code assist + if (invokeCodeCompletion()) { + IEditorPart editor = AdtUtils.getActiveEditor(); + if (editor instanceof AndroidXmlEditor) { + ((AndroidXmlEditor) editor).invokeContentAssist(-1); + } + } + } + + @Override + public boolean needsFocus() { + // Because we need to show the editor with text selected + return true; + } + + @Override + public Image getImage() { + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + return sharedImages.getImage(ISharedImages.IMG_OBJ_ADD); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetScrollViewSizeFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetScrollViewSizeFix.java new file mode 100644 index 000000000..52860cf85 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetScrollViewSizeFix.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; +import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; +import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW; +import static com.android.SdkConstants.VALUE_WRAP_CONTENT; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.text.IDocument; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +@SuppressWarnings("restriction") // DOM model +final class SetScrollViewSizeFix extends DocumentFix { + private SetScrollViewSizeFix(String id, IMarker marker) { + super(id, marker); + } + + @Override + public boolean needsFocus() { + return false; + } + + @Override + public boolean isCancelable() { + return false; + } + + @Override + protected void apply(IDocument document, IStructuredModel model, Node node, int start, + int end) { + if (node instanceof Element && node.getParentNode() instanceof Element) { + Element element = (Element) node; + Element parent = (Element) node.getParentNode(); + + boolean isHorizontal = HORIZONTAL_SCROLL_VIEW.equals(parent.getTagName()); + String attributeName = isHorizontal ? ATTR_LAYOUT_WIDTH : ATTR_LAYOUT_HEIGHT; + element.setAttributeNS(ANDROID_URI, attributeName, VALUE_WRAP_CONTENT); + } + } + + @Override + public String getDisplayString() { + return "Replace size attribute with wrap_content"; + } + + @Override + public Image getImage() { + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + // TODO: Need a better icon here + return sharedImages.getImage(ISharedImages.IMG_OBJ_ELEMENT); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/TypoFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/TypoFix.java new file mode 100644 index 000000000..7cc05d203 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/TypoFix.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.tools.lint.checks.TypoDetector; +import com.android.tools.lint.detector.api.TextFormat; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.FindReplaceDocumentAdapter; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.w3c.dom.Node; + +import java.util.ArrayList; +import java.util.List; + +/** Quickfix for fixing typos */ +@SuppressWarnings("restriction") // DOM model +final class TypoFix extends DocumentFix { + private String mTypo; + private String mReplacement; + + private TypoFix(String id, IMarker marker) { + super(id, marker); + } + + @Override + public String getDisplayString() { + return String.format("Replace \"%1$s\" by \"%2$s\"", mTypo, mReplacement); + } + + @Override + public boolean needsFocus() { + return false; + } + + @Override + public boolean isCancelable() { + return false; + } + + @Override + protected void apply(IDocument document, IStructuredModel model, Node node, + int start, int end) { + String message = mMarker.getAttribute(IMarker.MESSAGE, ""); + String typo = TypoDetector.getTypo(message, TextFormat.TEXT); + if (typo == null) { + return; + } + List<String> replacements = TypoDetector.getSuggestions(message, TextFormat.TEXT); + if (replacements == null || replacements.isEmpty()) { + return; + } + + try { + String current = document.get(start, end-start); + if (current.equals(typo)) { + document.replace(start, end - start, replacements.get(0)); + } else { + // The buffer has been edited; try to find the typo. + FindReplaceDocumentAdapter finder = new FindReplaceDocumentAdapter(document); + IRegion forward = finder.find(start, typo, true /*forward*/, true, true, false); + IRegion backward = finder.find(start, typo, false /*forward*/, true, true, false); + if (forward != null && backward != null) { + // Pick the closest one + int forwardDelta = forward.getOffset() - start; + int backwardDelta = start - backward.getOffset(); + if (forwardDelta < backwardDelta) { + start = forward.getOffset(); + } else { + start = backward.getOffset(); + } + } else if (forward != null) { + start = forward.getOffset(); + } else if (backward != null) { + start = backward.getOffset(); + } else { + return; + } + end = start + typo.length(); + document.replace(start, end - start, replacements.get(0)); + } + } catch (BadLocationException e) { + AdtPlugin.log(e, null); + } + } + + @Override + protected List<LintFix> getAllFixes() { + String message = mMarker.getAttribute(IMarker.MESSAGE, ""); + String typo = TypoDetector.getTypo(message, TextFormat.TEXT); + List<String> replacements = TypoDetector.getSuggestions(message, TextFormat.TEXT); + if (replacements != null && !replacements.isEmpty() && typo != null) { + List<LintFix> allFixes = new ArrayList<LintFix>(replacements.size()); + for (String replacement : replacements) { + TypoFix fix = new TypoFix(mId, mMarker); + fix.mTypo = typo; + fix.mReplacement = replacement; + allFixes.add(fix); + } + + return allFixes; + } + + return null; + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/TypographyFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/TypographyFix.java new file mode 100644 index 000000000..535e02350 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/TypographyFix.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ide.eclipse.adt.internal.lint; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.tools.lint.checks.TypographyDetector; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.List; + +@SuppressWarnings("restriction") // DOM model +final class TypographyFix extends DocumentFix { + private TypographyFix(String id, IMarker marker) { + super(id, marker); + } + + @Override + public boolean needsFocus() { + return false; + } + + @Override + public boolean isCancelable() { + return false; + } + + @Override + public boolean isBulkCapable() { + return false; + } + + @Override + protected void apply(IDocument document, IStructuredModel model, Node node, int start, + int end) { + if (node instanceof Element) { + Element element = (Element) node; + // Find the text node which contains the character in question + NodeList childNodes = element.getChildNodes(); + for (int i = 0, n = childNodes.getLength(); i < n; i++) { + Node child = childNodes.item(i); + if (child.getNodeType() == Node.TEXT_NODE) { + IndexedRegion region = (IndexedRegion) child; + String message = mMarker.getAttribute(IMarker.MESSAGE, ""); + List<TypographyDetector.ReplaceEdit> edits = + TypographyDetector.getEdits(mId, message, child); + for (TypographyDetector.ReplaceEdit edit : edits) { + try { + document.replace(edit.offset + region.getStartOffset(), + edit.length, edit.replaceWith); + } catch (BadLocationException e) { + AdtPlugin.log(e, null); + } + } + } + } + } + } + + @Override + public String getDisplayString() { + return "Replace with suggested characters"; + } + + @Override + public Image getImage() { + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + // TODO: Need a better icon here + return sharedImages.getImage(ISharedImages.IMG_OBJ_ELEMENT); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/UseCompoundDrawableDetectorFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/UseCompoundDrawableDetectorFix.java new file mode 100644 index 000000000..bf3cc9ac0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/UseCompoundDrawableDetectorFix.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.lint; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.UseCompoundDrawableRefactoring; +import com.android.tools.lint.checks.UseCompoundDrawableDetector; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextSelection; +import org.eclipse.jface.text.TextSelection; +import org.eclipse.ltk.ui.refactoring.RefactoringWizard; +import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.w3c.dom.Node; + +/** Quickfix for the {@link UseCompoundDrawableDetector} */ +@SuppressWarnings("restriction") // DOM model +class UseCompoundDrawableDetectorFix extends DocumentFix { + protected UseCompoundDrawableDetectorFix(String id, IMarker marker) { + super(id, marker); + } + + @Override + public String getDisplayString() { + return "Convert to a compound drawable"; + } + + @Override + public Image getImage() { + return AdtPlugin.getAndroidLogo(); + } + + @Override + public boolean needsFocus() { + return false; + } + + @Override + public boolean isCancelable() { + return false; + } + + @Override + public boolean isBulkCapable() { + return false; + } + + @Override + protected void apply(IDocument document, IStructuredModel model, Node node, + int start, int end) { + + // Invoke refactoring + LayoutEditorDelegate delegate = + LayoutEditorDelegate.fromEditor(AdtUtils.getActiveEditor()); + + if (delegate != null) { + IFile file = (IFile) mMarker.getResource(); + ITextSelection textSelection = new TextSelection(start, + end - start); + UseCompoundDrawableRefactoring refactoring = + new UseCompoundDrawableRefactoring(file, delegate, textSelection, null); + RefactoringWizard wizard = refactoring.createWizard(); + RefactoringWizardOpenOperation op = + new RefactoringWizardOpenOperation(wizard); + try { + IWorkbenchWindow window = PlatformUI.getWorkbench(). + getActiveWorkbenchWindow(); + op.run(window.getShell(), wizard.getDefaultPageTitle()); + } catch (InterruptedException e) { + } + } + } +} |