/* * 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.CONSTRUCTOR_NAME; 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.Collections; 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$ /** Constructing SimpleDateFormat without an explicit locale */ public static final Issue DATE_FORMAT = Issue.create( "SimpleDateFormat", //$NON-NLS-1$ "Implied locale in date format", "Almost all callers should use `getDateInstance()`, `getDateTimeInstance()`, or " + "`getTimeInstance()` to get a ready-made instance of SimpleDateFormat suitable " + "for the user's locale. The main reason you'd create an instance this class " + "directly is because you need to format/parse a specific machine-readable format, " + "in which case you almost certainly want to explicitly ask for US to ensure that " + "you get ASCII digits (rather than, say, Arabic digits).\n" + "\n" + "Therefore, you should either use the form of the SimpleDateFormat constructor " + "where you pass in an explicit locale, such as Locale.US, or use one of the " + "get instance methods, or suppress this error if really know what you are doing.", Category.CORRECTNESS, 6, Severity.WARNING, IMPLEMENTATION) .addMoreInfo( "http://developer.android.com/reference/java/text/SimpleDateFormat.html"); //$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 @Nullable public List<String> getApplicableCallOwners() { return Collections.singletonList(DATE_FORMAT_OWNER); } @Override public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, @NonNull MethodNode method, @NonNull MethodInsnNode call) { String owner = call.owner; String desc = call.desc; String name = call.name; if (owner.equals(DATE_FORMAT_OWNER)) { if (!name.equals(CONSTRUCTOR_NAME)) { return; } if (desc.equals("(Ljava/lang/String;Ljava/text/DateFormatSymbols;)V") //$NON-NLS-1$ || desc.equals("()V") //$NON-NLS-1$ || desc.equals("(Ljava/lang/String;)V")) { //$NON-NLS-1$ Location location = context.getLocation(call); String message = "To get local formatting use `getDateInstance()`, `getDateTimeInstance()`, " + "or `getTimeInstance()`, or use `new SimpleDateFormat(String template, " + "Locale locale)` with for example `Locale.US` for ASCII dates."; context.report(DATE_FORMAT, method, call, location, message); } return; } else if (!owner.equals(STRING_OWNER)) { return; } 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; } } }