aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddPrefixFix.java69
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAnnotation.java424
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java237
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ClearLintMarkersAction.java44
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ColumnDialog.java124
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ConvertToDpFix.java113
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/DocumentFix.java63
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/DosLineEndingsFix.java64
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java1306
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintIssueRegistry.java65
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintRunner.java238
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ExtractStringFix.java90
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/GlobalLintConfiguration.java176
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/InputDensityDialog.java118
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LinearLayoutWeightFix.java78
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintColumn.java532
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintDeltaProcessor.java201
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintEditAction.java49
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFix.java226
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFixGenerator.java563
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintJob.java201
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintList.java979
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintListDialog.java303
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintViewPart.java657
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ObsoleteLayoutParamsFix.java80
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ProjectLintConfiguration.java90
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/RemoveUselessViewFix.java99
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/RunLintAction.java232
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetAttributeFix.java151
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetPropertyFix.java140
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetScrollViewSizeFix.java73
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/TypoFix.java123
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/TypographyFix.java94
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/UseCompoundDrawableDetectorFix.java95
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 &lt;string&gt;.</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) {
+ }
+ }
+ }
+}