| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tools.lint.checks; |
| |
| import static com.android.SdkConstants.FORMAT_METHOD; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.tools.lint.detector.api.Category; |
| import com.android.tools.lint.detector.api.ClassContext; |
| import com.android.tools.lint.detector.api.Detector; |
| import com.android.tools.lint.detector.api.Detector.ClassScanner; |
| import com.android.tools.lint.detector.api.Implementation; |
| import com.android.tools.lint.detector.api.Issue; |
| import com.android.tools.lint.detector.api.Location; |
| import com.android.tools.lint.detector.api.Scope; |
| import com.android.tools.lint.detector.api.Severity; |
| import com.android.tools.lint.detector.api.Speed; |
| |
| import org.objectweb.asm.Opcodes; |
| import org.objectweb.asm.tree.AbstractInsnNode; |
| import org.objectweb.asm.tree.ClassNode; |
| import org.objectweb.asm.tree.InsnList; |
| import org.objectweb.asm.tree.LdcInsnNode; |
| import org.objectweb.asm.tree.MethodInsnNode; |
| import org.objectweb.asm.tree.MethodNode; |
| import org.objectweb.asm.tree.analysis.Analyzer; |
| import org.objectweb.asm.tree.analysis.AnalyzerException; |
| import org.objectweb.asm.tree.analysis.Frame; |
| import org.objectweb.asm.tree.analysis.SourceInterpreter; |
| import org.objectweb.asm.tree.analysis.SourceValue; |
| |
| import java.util.Arrays; |
| import java.util.List; |
| |
| /** |
| * Checks for errors related to locale handling |
| */ |
| public class LocaleDetector extends Detector implements ClassScanner { |
| private static final Implementation IMPLEMENTATION = new Implementation( |
| LocaleDetector.class, |
| Scope.CLASS_FILE_SCOPE); |
| |
| /** Calling risky convenience methods */ |
| public static final Issue STRING_LOCALE = Issue.create( |
| "DefaultLocale", //$NON-NLS-1$ |
| "Implied default locale in case conversion", |
| |
| "Calling `String#toLowerCase()` or `#toUpperCase()` *without specifying an " + |
| "explicit locale* is a common source of bugs. The reason for that is that those " + |
| "methods will use the current locale on the user's device, and even though the " + |
| "code appears to work correctly when you are developing the app, it will fail " + |
| "in some locales. For example, in the Turkish locale, the uppercase replacement " + |
| "for `i` is *not* `I`.\n" + |
| "\n" + |
| "If you want the methods to just perform ASCII replacement, for example to convert " + |
| "an enum name, call `String#toUpperCase(Locale.US)` instead. If you really want to " + |
| "use the current locale, call `String#toUpperCase(Locale.getDefault())` instead.", |
| |
| Category.CORRECTNESS, |
| 6, |
| Severity.WARNING, |
| IMPLEMENTATION) |
| .addMoreInfo( |
| "http://developer.android.com/reference/java/util/Locale.html#default_locale"); //$NON-NLS-1$ |
| |
| static final String DATE_FORMAT_OWNER = "java/text/SimpleDateFormat"; //$NON-NLS-1$ |
| private static final String STRING_OWNER = "java/lang/String"; //$NON-NLS-1$ |
| |
| /** Constructs a new {@link LocaleDetector} */ |
| public LocaleDetector() { |
| } |
| |
| @NonNull |
| @Override |
| public Speed getSpeed() { |
| return Speed.FAST; |
| } |
| |
| // ---- Implements ClassScanner ---- |
| |
| @Override |
| @Nullable |
| public List<String> getApplicableCallNames() { |
| return Arrays.asList( |
| "toLowerCase", //$NON-NLS-1$ |
| "toUpperCase", //$NON-NLS-1$ |
| FORMAT_METHOD |
| ); |
| } |
| |
| @Override |
| public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, |
| @NonNull MethodNode method, @NonNull MethodInsnNode call) { |
| String owner = call.owner; |
| if (!owner.equals(STRING_OWNER)) { |
| return; |
| } |
| |
| String desc = call.desc; |
| String name = call.name; |
| |
| if (name.equals(FORMAT_METHOD)) { |
| // Only check the non-locale version of String.format |
| if (!desc.equals("(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;")) { //$NON-NLS-1$ |
| return; |
| } |
| // Find the formatting string |
| Analyzer analyzer = new Analyzer(new SourceInterpreter() { |
| @Override |
| public SourceValue newOperation(AbstractInsnNode insn) { |
| if (insn.getOpcode() == Opcodes.LDC) { |
| Object cst = ((LdcInsnNode) insn).cst; |
| if (cst instanceof String) { |
| return new StringValue(1, (String) cst); |
| } |
| } |
| return super.newOperation(insn); |
| } |
| }); |
| try { |
| Frame[] frames = analyzer.analyze(classNode.name, method); |
| InsnList instructions = method.instructions; |
| Frame frame = frames[instructions.indexOf(call)]; |
| if (frame.getStackSize() == 0) { |
| return; |
| } |
| SourceValue stackValue = (SourceValue) frame.getStack(0); |
| if (stackValue instanceof StringValue) { |
| String format = ((StringValue) stackValue).getString(); |
| if (format != null && StringFormatDetector.isLocaleSpecific(format)) { |
| Location location = context.getLocation(call); |
| String message = |
| "Implicitly using the default locale is a common source of bugs: " + |
| "Use `String.format(Locale, ...)` instead"; |
| context.report(STRING_LOCALE, method, call, location, message); |
| } |
| } |
| } catch (AnalyzerException e) { |
| context.log(e, null); |
| } |
| } else { |
| if (desc.equals("()Ljava/lang/String;")) { //$NON-NLS-1$ |
| Location location = context.getLocation(call); |
| String message = String.format( |
| "Implicitly using the default locale is a common source of bugs: " + |
| "Use `%1$s(Locale)` instead", name); |
| context.report(STRING_LOCALE, method, call, location, message); |
| } |
| } |
| } |
| |
| private static class StringValue extends SourceValue { |
| private final String mString; |
| |
| StringValue(int size, String string) { |
| super(size); |
| mString = string; |
| } |
| |
| String getString() { |
| return mString; |
| } |
| |
| @Override |
| public int getSize() { |
| return 1; |
| } |
| } |
| } |