| package org.checkerframework.checker.i18nformatter; |
| |
| import com.sun.source.tree.LiteralTree; |
| import com.sun.source.tree.Tree; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.InputStream; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Properties; |
| import java.util.ResourceBundle; |
| import javax.lang.model.element.AnnotationMirror; |
| import org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory; |
| import org.checkerframework.checker.i18nformatter.qual.I18nFormat; |
| import org.checkerframework.checker.i18nformatter.qual.I18nFormatBottom; |
| import org.checkerframework.checker.i18nformatter.qual.I18nFormatFor; |
| import org.checkerframework.checker.i18nformatter.qual.I18nInvalidFormat; |
| import org.checkerframework.checker.i18nformatter.qual.I18nUnknownFormat; |
| import org.checkerframework.checker.i18nformatter.util.I18nFormatUtil; |
| import org.checkerframework.checker.signature.qual.CanonicalName; |
| import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; |
| import org.checkerframework.common.basetype.BaseTypeChecker; |
| import org.checkerframework.framework.type.AnnotatedTypeFactory; |
| import org.checkerframework.framework.type.AnnotatedTypeMirror; |
| import org.checkerframework.framework.type.MostlyNoElementQualifierHierarchy; |
| import org.checkerframework.framework.type.QualifierHierarchy; |
| import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; |
| import org.checkerframework.framework.type.treeannotator.TreeAnnotator; |
| import org.checkerframework.framework.util.QualifierKind; |
| import org.checkerframework.javacutil.AnnotationBuilder; |
| import org.checkerframework.javacutil.AnnotationUtils; |
| import org.checkerframework.javacutil.BugInCF; |
| import org.plumelib.reflection.Signatures; |
| |
| /** |
| * Adds {@link I18nFormat} to the type of tree, if it is a {@code String} or {@code char} literal |
| * that represents a satisfiable format. The annotation's value is set to be a list of appropriate |
| * {@link I18nConversionCategory} values for every parameter of the format. |
| * |
| * <p>It also creates a map from the provided translation file if exists. This map will be used to |
| * get the corresponding value of a key when {@link java.util.ResourceBundle#getString} method is |
| * invoked. |
| * |
| * @checker_framework.manual #i18n-formatter-checker Internationalization Format String Checker |
| */ |
| public class I18nFormatterAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { |
| |
| /** The @{@link I18nUnknownFormat} annotation. */ |
| protected final AnnotationMirror I18NUNKNOWNFORMAT = |
| AnnotationBuilder.fromClass(elements, I18nUnknownFormat.class); |
| /** The @{@link I18nFormatBottom} annotation. */ |
| protected final AnnotationMirror I18NFORMATBOTTOM = |
| AnnotationBuilder.fromClass(elements, I18nFormatBottom.class); |
| |
| /** The fully-qualified name of {@link I18nFormat}. */ |
| protected static final @CanonicalName String I18NFORMAT_NAME = |
| I18nFormat.class.getCanonicalName(); |
| /** The fully-qualified name of {@link I18nInvalidFormat}. */ |
| protected static final @CanonicalName String I18NINVALIDFORMAT_NAME = |
| I18nInvalidFormat.class.getCanonicalName(); |
| /** The fully-qualified name of {@link I18nFormatFor}. */ |
| protected static final @CanonicalName String I18NFORMATFOR_NAME = |
| I18nFormatFor.class.getCanonicalName(); |
| |
| /** Map from a translation file key to its value in the file. */ |
| public final Map<String, String> translations = Collections.unmodifiableMap(buildLookup()); |
| |
| /** Syntax tree utilities. */ |
| protected final I18nFormatterTreeUtil treeUtil = new I18nFormatterTreeUtil(checker); |
| |
| /** Create a new I18nFormatterAnnotatedTypeFactory. */ |
| public I18nFormatterAnnotatedTypeFactory(BaseTypeChecker checker) { |
| super(checker); |
| |
| this.postInit(); |
| } |
| |
| /** |
| * Builds a map from a translation file key to its value in the file. |
| * |
| * @return a map from a translation file key to its value in the file |
| */ |
| private Map<String, String> buildLookup() { |
| Map<String, String> result = new HashMap<>(); |
| |
| if (checker.hasOption("propfiles")) { |
| String names = checker.getOption("propfiles"); |
| String[] namesArr = names.split(":"); |
| |
| if (namesArr == null) { |
| System.err.println("Couldn't parse the properties files: <" + names + ">"); |
| } else { |
| for (String name : namesArr) { |
| try { |
| Properties prop = new Properties(); |
| |
| ClassLoader cl = this.getClass().getClassLoader(); |
| if (cl == null) { |
| // The class loader is null if the system class loader was used. |
| cl = ClassLoader.getSystemClassLoader(); |
| } |
| InputStream in = cl.getResourceAsStream(name); |
| |
| if (in == null) { |
| // If the classloader didn't manage to load the file, try whether a |
| // FileInputStream works. For absolute paths this might help. |
| try { |
| in = new FileInputStream(name); |
| } catch (FileNotFoundException e) { |
| // ignore |
| } |
| } |
| |
| if (in == null) { |
| System.err.println("Couldn't find the properties file: " + name); |
| // report(null, "propertykeychecker.filenotfound", name); |
| // return Collections.emptySet(); |
| continue; |
| } |
| |
| prop.load(in); |
| |
| for (String key : prop.stringPropertyNames()) { |
| result.put(key, prop.getProperty(key)); |
| } |
| } catch (Exception e) { |
| // TODO: is there a nicer way to report messages, that are not connected to |
| // an AST node? One cannot use report, because it needs a node. |
| System.err.println("Exception in PropertyKeyChecker.keysOfPropertyFile: " + e); |
| e.printStackTrace(); |
| } |
| } |
| } |
| } |
| |
| if (checker.hasOption("bundlenames")) { |
| String bundleNames = checker.getOption("bundlenames"); |
| String[] namesArr = bundleNames.split(":"); |
| |
| if (namesArr == null) { |
| System.err.println("Couldn't parse the resource bundles: <" + bundleNames + ">"); |
| } else { |
| for (String bundleName : namesArr) { |
| if (!Signatures.isBinaryName(bundleName)) { |
| System.err.println( |
| "Malformed resource bundle: <" + bundleName + "> should be a binary name."); |
| continue; |
| } |
| ResourceBundle bundle = ResourceBundle.getBundle(bundleName); |
| if (bundle == null) { |
| System.err.println( |
| "Couldn't find the resource bundle: <" |
| + bundleName |
| + "> for locale <" |
| + Locale.getDefault() |
| + ">."); |
| continue; |
| } |
| |
| for (String key : bundle.keySet()) { |
| result.put(key, bundle.getString(key)); |
| } |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| @Override |
| protected QualifierHierarchy createQualifierHierarchy() { |
| return new I18nFormatterQualifierHierarchy(); |
| } |
| |
| @Override |
| public TreeAnnotator createTreeAnnotator() { |
| return new ListTreeAnnotator(super.createTreeAnnotator(), new I18nFormatterTreeAnnotator(this)); |
| } |
| |
| private class I18nFormatterTreeAnnotator extends TreeAnnotator { |
| public I18nFormatterTreeAnnotator(AnnotatedTypeFactory atypeFactory) { |
| super(atypeFactory); |
| } |
| |
| @Override |
| public Void visitLiteral(LiteralTree tree, AnnotatedTypeMirror type) { |
| if (!type.isAnnotatedInHierarchy(I18NUNKNOWNFORMAT)) { |
| String format = null; |
| if (tree.getKind() == Tree.Kind.STRING_LITERAL) { |
| format = (String) tree.getValue(); |
| } else if (tree.getKind() == Tree.Kind.CHAR_LITERAL) { |
| format = Character.toString((Character) tree.getValue()); |
| } |
| if (format != null) { |
| AnnotationMirror anno; |
| try { |
| I18nConversionCategory[] cs = I18nFormatUtil.formatParameterCategories(format); |
| anno = I18nFormatterAnnotatedTypeFactory.this.treeUtil.categoriesToFormatAnnotation(cs); |
| } catch (IllegalArgumentException e) { |
| anno = |
| I18nFormatterAnnotatedTypeFactory.this.treeUtil.exceptionToInvalidFormatAnnotation( |
| e); |
| } |
| type.addAnnotation(anno); |
| } |
| } |
| |
| return super.visitLiteral(tree, type); |
| } |
| } |
| |
| /** I18nFormatterQualifierHierarchy. */ |
| class I18nFormatterQualifierHierarchy extends MostlyNoElementQualifierHierarchy { |
| |
| /** Qualifier kind for the @{@link I18nFormat} annotation. */ |
| private final QualifierKind I18NFORMAT_KIND; |
| /** Qualifier kind for the @{@link I18nFormatFor} annotation. */ |
| private final QualifierKind I18NFORMATFOR_KIND; |
| /** Qualifier kind for the @{@link I18nInvalidFormat} annotation. */ |
| private final QualifierKind I18NINVALIDFORMAT_KIND; |
| |
| /** Creates I18nFormatterQualifierHierarchy. */ |
| public I18nFormatterQualifierHierarchy() { |
| super(I18nFormatterAnnotatedTypeFactory.this.getSupportedTypeQualifiers(), elements); |
| this.I18NFORMAT_KIND = this.getQualifierKind(I18NFORMAT_NAME); |
| this.I18NFORMATFOR_KIND = this.getQualifierKind(I18NFORMATFOR_NAME); |
| this.I18NINVALIDFORMAT_KIND = this.getQualifierKind(I18NINVALIDFORMAT_NAME); |
| } |
| |
| @Override |
| protected boolean isSubtypeWithElements( |
| AnnotationMirror subAnno, |
| QualifierKind subKind, |
| AnnotationMirror superAnno, |
| QualifierKind superKind) { |
| if (subKind == I18NFORMAT_KIND && superKind == I18NFORMAT_KIND) { |
| |
| I18nConversionCategory[] rhsArgTypes = treeUtil.formatAnnotationToCategories(subAnno); |
| I18nConversionCategory[] lhsArgTypes = treeUtil.formatAnnotationToCategories(superAnno); |
| |
| if (rhsArgTypes.length > lhsArgTypes.length) { |
| return false; |
| } |
| |
| for (int i = 0; i < rhsArgTypes.length; ++i) { |
| if (!I18nConversionCategory.isSubsetOf(lhsArgTypes[i], rhsArgTypes[i])) { |
| return false; |
| } |
| } |
| return true; |
| } else if ((subKind == I18NINVALIDFORMAT_KIND && superKind == I18NINVALIDFORMAT_KIND) |
| || (subKind == I18NFORMATFOR_KIND && superKind == I18NFORMATFOR_KIND)) { |
| return Objects.equals( |
| treeUtil.getI18nInvalidFormatValue(subAnno), |
| treeUtil.getI18nInvalidFormatValue(superAnno)); |
| } |
| throw new BugInCF("Unexpected QualifierKinds: %s %s", subKind, superKind); |
| } |
| |
| @Override |
| protected AnnotationMirror leastUpperBoundWithElements( |
| AnnotationMirror anno1, |
| QualifierKind qualifierKind1, |
| AnnotationMirror anno2, |
| QualifierKind qualifierKind2, |
| QualifierKind lubKind) { |
| if (qualifierKind1.isBottom()) { |
| return anno2; |
| } else if (qualifierKind2.isBottom()) { |
| return anno1; |
| } else if (qualifierKind1 == I18NFORMAT_KIND && qualifierKind2 == I18NFORMAT_KIND) { |
| I18nConversionCategory[] shorterArgTypesList = treeUtil.formatAnnotationToCategories(anno1); |
| I18nConversionCategory[] longerArgTypesList = treeUtil.formatAnnotationToCategories(anno2); |
| if (shorterArgTypesList.length > longerArgTypesList.length) { |
| I18nConversionCategory[] temp = longerArgTypesList; |
| longerArgTypesList = shorterArgTypesList; |
| shorterArgTypesList = temp; |
| } |
| |
| // From the manual: |
| // It is legal to use a format string with fewer format specifiers |
| // than required, but a warning is issued. |
| |
| I18nConversionCategory[] resultArgTypes = |
| new I18nConversionCategory[longerArgTypesList.length]; |
| |
| for (int i = 0; i < shorterArgTypesList.length; ++i) { |
| resultArgTypes[i] = |
| I18nConversionCategory.intersect(shorterArgTypesList[i], longerArgTypesList[i]); |
| } |
| for (int i = shorterArgTypesList.length; i < longerArgTypesList.length; ++i) { |
| resultArgTypes[i] = longerArgTypesList[i]; |
| } |
| return treeUtil.categoriesToFormatAnnotation(resultArgTypes); |
| } else if (qualifierKind1 == I18NINVALIDFORMAT_KIND |
| && qualifierKind2 == I18NINVALIDFORMAT_KIND) { |
| assert !anno1.getElementValues().isEmpty(); |
| assert !anno1.getElementValues().isEmpty(); |
| |
| if (AnnotationUtils.areSame(anno1, anno2)) { |
| return anno1; |
| } |
| |
| return treeUtil.stringToInvalidFormatAnnotation( |
| "(" |
| + treeUtil.invalidFormatAnnotationToErrorMessage(anno1) |
| + " or " |
| + treeUtil.invalidFormatAnnotationToErrorMessage(anno2) |
| + ")"); |
| } else if (qualifierKind1 == I18NFORMATFOR_KIND && AnnotationUtils.areSame(anno1, anno2)) { |
| // @I18nFormatFor annotations are unrelated by subtyping, unless they are identical. |
| return anno1; |
| } |
| |
| return I18NUNKNOWNFORMAT; |
| } |
| |
| @Override |
| protected AnnotationMirror greatestLowerBoundWithElements( |
| AnnotationMirror anno1, |
| QualifierKind qualifierKind1, |
| AnnotationMirror anno2, |
| QualifierKind qualifierKind2, |
| QualifierKind glbKind) { |
| if (qualifierKind1.isTop()) { |
| return anno2; |
| } else if (qualifierKind2.isTop()) { |
| return anno1; |
| } else if (qualifierKind1 == I18NFORMAT_KIND && qualifierKind2 == I18NFORMAT_KIND) { |
| I18nConversionCategory[] anno1ArgTypes = treeUtil.formatAnnotationToCategories(anno1); |
| I18nConversionCategory[] anno2ArgTypes = treeUtil.formatAnnotationToCategories(anno2); |
| |
| // From the manual: |
| // It is legal to use a format string with fewer format specifiers |
| // than required, but a warning is issued. |
| int length = anno1ArgTypes.length; |
| if (anno2ArgTypes.length < length) { |
| length = anno2ArgTypes.length; |
| } |
| |
| I18nConversionCategory[] anno3ArgTypes = new I18nConversionCategory[length]; |
| |
| for (int i = 0; i < length; ++i) { |
| anno3ArgTypes[i] = I18nConversionCategory.union(anno1ArgTypes[i], anno2ArgTypes[i]); |
| } |
| return treeUtil.categoriesToFormatAnnotation(anno3ArgTypes); |
| } else if (qualifierKind1 == I18NINVALIDFORMAT_KIND |
| && qualifierKind2 == I18NINVALIDFORMAT_KIND) { |
| |
| assert !anno2.getElementValues().isEmpty(); |
| |
| if (AnnotationUtils.areSame(anno1, anno2)) { |
| return anno1; |
| } |
| |
| return treeUtil.stringToInvalidFormatAnnotation( |
| "(" |
| + treeUtil.invalidFormatAnnotationToErrorMessage(anno1) |
| + " and " |
| + treeUtil.invalidFormatAnnotationToErrorMessage(anno2) |
| + ")"); |
| } else if (qualifierKind1 == I18NFORMATFOR_KIND && AnnotationUtils.areSame(anno1, anno2)) { |
| // @I18nFormatFor annotations are unrelated by subtyping, unless they are identical. |
| return anno1; |
| } |
| |
| return I18NFORMATBOTTOM; |
| } |
| } |
| } |