1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.tools.lint.checks;
18
19import com.android.annotations.NonNull;
20import com.android.annotations.Nullable;
21import com.android.tools.lint.detector.api.Category;
22import com.android.tools.lint.detector.api.Context;
23import com.android.tools.lint.detector.api.Detector;
24import com.android.tools.lint.detector.api.Issue;
25import com.android.tools.lint.detector.api.JavaContext;
26import com.android.tools.lint.detector.api.Scope;
27import com.android.tools.lint.detector.api.Severity;
28
29import java.io.File;
30import java.util.Collections;
31import java.util.List;
32
33import lombok.ast.AstVisitor;
34import lombok.ast.ConstructorDeclaration;
35import lombok.ast.Expression;
36import lombok.ast.ForwardingAstVisitor;
37import lombok.ast.IntegralLiteral;
38import lombok.ast.MethodDeclaration;
39import lombok.ast.MethodInvocation;
40import lombok.ast.Node;
41import lombok.ast.Return;
42import lombok.ast.StrictListAccessor;
43
44/** Detector looking for Toast.makeText() without a corresponding show() call */
45public class ToastDetector extends Detector implements Detector.JavaScanner {
46    /** The main issue discovered by this detector */
47    public static final Issue ISSUE = Issue.create(
48            "ShowToast", //$NON-NLS-1$
49            "Looks for code creating a Toast but forgetting to call show() on it",
50
51            "`Toast.makeText()` creates a `Toast` but does *not* show it. You must call " +
52            "`show()` on the resulting object to actually make the `Toast` appear.",
53
54            Category.CORRECTNESS,
55            6,
56            Severity.WARNING,
57            ToastDetector.class,
58            Scope.JAVA_FILE_SCOPE);
59
60
61    /** Constructs a new {@link ToastDetector} check */
62    public ToastDetector() {
63    }
64
65    @Override
66    public boolean appliesTo(@NonNull Context context, @NonNull File file) {
67        return true;
68    }
69
70
71    // ---- Implements JavaScanner ----
72
73    @Override
74    public List<String> getApplicableMethodNames() {
75        return Collections.singletonList("makeText"); //$NON-NLS-1$
76    }
77
78    private Node findSurroundingMethod(Node scope) {
79        while (scope != null) {
80            Class<? extends Node> type = scope.getClass();
81            // The Lombok AST uses a flat hierarchy of node type implementation classes
82            // so no need to do instanceof stuff here.
83            if (type == MethodDeclaration.class || type == ConstructorDeclaration.class) {
84                return scope;
85            }
86
87            scope = scope.getParent();
88        }
89
90        return null;
91    }
92
93    @Override
94    public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
95            @NonNull MethodInvocation node) {
96        assert node.astName().astValue().equals("makeText");
97        if (node.astOperand() == null) {
98            // "makeText()" in the code with no operand
99            return;
100        }
101
102        String operand = node.astOperand().toString();
103        if (!(operand.equals("Toast") || operand.endsWith(".Toast"))) {
104            return;
105        }
106
107        // Make sure you pass the right kind of duration: it's not a delay, it's
108        //  LENGTH_SHORT or LENGTH_LONG
109        // (see http://code.google.com/p/android/issues/detail?id=3655)
110        StrictListAccessor<Expression, MethodInvocation> args = node.astArguments();
111        if (args.size() == 3) {
112            Expression duration = args.last();
113            if (duration instanceof IntegralLiteral) {
114                context.report(ISSUE, context.getLocation(duration),
115                        "Expected duration Toast.LENGTH_SHORT or Toast.LENGTH_LONG, a custom " +
116                        "duration value is not supported",
117                        null);
118            }
119        }
120
121        Node method = findSurroundingMethod(node.getParent());
122        if (method == null) {
123            return;
124        }
125
126        ShowFinder finder = new ShowFinder(node);
127        method.accept(finder);
128        if (!finder.isShowCalled()) {
129            context.report(ISSUE, method, context.getLocation(node),
130                    "Toast created but not shown: did you forget to call show() ?", null);
131        }
132    }
133
134    private class ShowFinder extends ForwardingAstVisitor {
135        /** Whether we've found the show method */
136        private boolean mFound;
137        /** The target makeText call */
138        private MethodInvocation mTarget;
139        /** Whether we've seen the target makeText node yet */
140        private boolean mSeenTarget;
141
142        private ShowFinder(MethodInvocation target) {
143            mTarget = target;
144        }
145
146        @Override
147        public boolean visitMethodInvocation(MethodInvocation node) {
148            if (node == mTarget) {
149                mSeenTarget = true;
150            } else if ((mSeenTarget || node.astOperand() == mTarget)
151                    && "show".equals(node.astName().astValue())) { //$NON-NLS-1$
152                // TODO: Do more flow analysis to see whether we're really calling show
153                // on the right type of object?
154                mFound = true;
155            }
156
157            return true;
158        }
159
160        @Override
161        public boolean visitReturn(Return node) {
162            if (node.astValue() == mTarget) {
163                // If you just do "return Toast.makeText(...) don't warn
164                mFound = true;
165            }
166            return super.visitReturn(node);
167        }
168
169        boolean isShowCalled() {
170            return mFound;
171        }
172    }
173}
174