From 126cc778e46de1878408f3ff24ab8f63eb8e377f Mon Sep 17 00:00:00 2001 From: Yan Zhulanow Date: Thu, 31 Mar 2016 17:34:50 +0300 Subject: [PATCH] Lint: copy diagnostics from IDEA (br143) --- .../tools/klint/client/api/AsmVisitor.java | 202 ++ .../api/CircularDependencyException.java | 81 + .../tools/klint/client/api/ClassEntry.java | 335 +++ .../client/api/CompositeIssueRegistry.java | 52 + .../tools/klint/client/api/Configuration.java | 124 + .../client/api/DefaultConfiguration.java | 609 ++++ .../klint/client/api/DefaultSdkInfo.java | 288 ++ .../tools/klint/client/api/IssueRegistry.java | 322 +++ .../client/api/JarFileIssueRegistry.java | 113 + .../tools/klint/client/api/JavaParser.java | 530 ++++ .../tools/klint/client/api/JavaVisitor.java | 1398 +++++++++ .../tools/klint/client/api/LintClient.java | 1053 +++++++ .../tools/klint/client/api/LintDriver.java | 2535 +++++++++++++++++ .../tools/klint/client/api/LintListener.java | 70 + .../tools/klint/client/api/LintRequest.java | 179 ++ .../klint/client/api/OtherFileVisitor.java | 207 ++ .../klint/client/api/ResourceVisitor.java | 245 ++ .../tools/klint/client/api/SdkInfo.java | 90 + .../tools/klint/client/api/XmlParser.java | 137 + .../tools/klint/detector/api/Category.java | 155 + .../klint/detector/api/ClassContext.java | 720 +++++ .../tools/klint/detector/api/Context.java | 441 +++ .../klint/detector/api/DefaultPosition.java | 68 + .../tools/klint/detector/api/Detector.java | 778 +++++ .../klint/detector/api/Implementation.java | 189 ++ .../tools/klint/detector/api/Issue.java | 307 ++ .../tools/klint/detector/api/JavaContext.java | 394 +++ .../klint/detector/api/LayoutDetector.java | 70 + .../tools/klint/detector/api/LintUtils.java | 1265 ++++++++ .../tools/klint/detector/api/Location.java | 777 +++++ .../tools/klint/detector/api/Position.java | 50 + .../tools/klint/detector/api/Project.java | 1348 +++++++++ .../klint/detector/api/ResourceContext.java | 80 + .../detector/api/ResourceXmlDetector.java | 61 + .../tools/klint/detector/api/Scope.java | 264 ++ .../tools/klint/detector/api/Severity.java | 103 + .../tools/klint/detector/api/Speed.java | 58 + .../tools/klint/detector/api/TextFormat.java | 300 ++ .../tools/klint/detector/api/XmlContext.java | 205 ++ .../klint/checks/AccessibilityDetector.java | 132 + .../AddJavascriptInterfaceDetector.java | 106 + .../tools/klint/checks/AlarmDetector.java | 136 + .../checks/AlwaysShowActionDetector.java | 239 ++ .../klint/checks/AnnotationDetector.java | 206 ++ .../com/android/tools/klint/checks/Api.java | 87 + .../android/tools/klint/checks/ApiClass.java | 424 +++ .../tools/klint/checks/ApiDetector.java | 2181 ++++++++++++++ .../android/tools/klint/checks/ApiLookup.java | 996 +++++++ .../android/tools/klint/checks/ApiParser.java | 122 + .../klint/checks/AppCompatCallDetector.java | 187 ++ .../checks/AppCompatResourceDetector.java | 109 + .../klint/checks/AppIndexingApiDetector.java | 287 ++ .../tools/klint/checks/ArraySizeDetector.java | 324 +++ .../tools/klint/checks/AssertDetector.java | 142 + .../klint/checks/BuiltinIssueRegistry.java | 308 ++ .../tools/klint/checks/ButtonDetector.java | 784 +++++ .../klint/checks/ByteOrderMarkDetector.java | 90 + .../tools/klint/checks/CallSuperDetector.java | 223 ++ .../klint/checks/ChildCountDetector.java | 131 + .../checks/CipherGetInstanceDetector.java | 127 + .../tools/klint/checks/CleanupDetector.java | 601 ++++ .../ClickableViewAccessibilityDetector.java | 265 ++ .../tools/klint/checks/CommentDetector.java | 199 ++ .../tools/klint/checks/ControlFlowGraph.java | 533 ++++ .../klint/checks/CustomViewDetector.java | 171 ++ .../tools/klint/checks/CutPasteDetector.java | 242 ++ .../klint/checks/DateFormatDetector.java | 115 + .../klint/checks/DeprecationDetector.java | 177 ++ .../klint/checks/DetectMissingPrefix.java | 196 ++ .../klint/checks/DosLineEndingDetector.java | 117 + .../klint/checks/DuplicateIdDetector.java | 674 +++++ .../checks/DuplicateResourceDetector.java | 295 ++ .../tools/klint/checks/ExtraTextDetector.java | 147 + .../klint/checks/FieldGetterDetector.java | 270 ++ .../tools/klint/checks/FragmentDetector.java | 165 ++ .../checks/FullBackupContentDetector.java | 234 ++ .../klint/checks/GetSignaturesDetector.java | 156 + .../tools/klint/checks/GradleDetector.java | 1173 ++++++++ .../klint/checks/GridLayoutDetector.java | 246 ++ .../tools/klint/checks/HandlerDetector.java | 139 + .../checks/HardcodedDebugModeDetector.java | 97 + .../klint/checks/HardcodedValuesDetector.java | 117 + .../tools/klint/checks/IconDetector.java | 2107 ++++++++++++++ .../tools/klint/checks/IncludeDetector.java | 148 + .../checks/InefficientWeightDetector.java | 387 +++ .../klint/checks/InvalidPackageDetector.java | 309 ++ .../klint/checks/JavaPerformanceDetector.java | 594 ++++ .../checks/JavaScriptInterfaceDetector.java | 321 +++ .../tools/klint/checks/LabelForDetector.java | 171 ++ .../checks/LayoutConsistencyDetector.java | 446 +++ .../klint/checks/LayoutInflationDetector.java | 269 ++ .../tools/klint/checks/LocaleDetector.java | 184 ++ .../klint/checks/LocaleFolderDetector.java | 437 +++ .../tools/klint/checks/LogDetector.java | 327 +++ .../tools/klint/checks/ManifestDetector.java | 994 +++++++ .../klint/checks/ManifestTypoDetector.java | 184 ++ .../tools/klint/checks/MathDetector.java | 100 + .../checks/MergeRootFrameLayoutDetector.java | 217 ++ .../klint/checks/MissingClassDetector.java | 527 ++++ .../tools/klint/checks/MissingIdDetector.java | 94 + .../tools/klint/checks/NamespaceDetector.java | 281 ++ .../klint/checks/NegativeMarginDetector.java | 246 ++ .../checks/NestedScrollingWidgetDetector.java | 158 + .../klint/checks/NfcTechListDetector.java | 110 + .../NonInternationalizedSmsDetector.java | 104 + .../checks/ObsoleteLayoutParamsDetector.java | 439 +++ .../tools/klint/checks/OnClickDetector.java | 244 ++ .../tools/klint/checks/OverdrawDetector.java | 586 ++++ .../checks/OverrideConcreteDetector.java | 163 ++ .../tools/klint/checks/OverrideDetector.java | 280 ++ .../tools/klint/checks/ParcelDetector.java | 138 + .../tools/klint/checks/PermissionHolder.java | 101 + .../klint/checks/PermissionRequirement.java | 694 +++++ .../tools/klint/checks/PluralsDatabase.java | 329 +++ .../tools/klint/checks/PluralsDetector.java | 238 ++ .../checks/PreferenceActivityDetector.java | 169 ++ .../klint/checks/PrivateKeyDetector.java | 107 + .../klint/checks/PrivateResourceDetector.java | 323 +++ .../tools/klint/checks/ProguardDetector.java | 166 ++ .../klint/checks/PropertyFileDetector.java | 193 ++ .../tools/klint/checks/PxUsageDetector.java | 383 +++ .../klint/checks/RegistrationDetector.java | 263 ++ .../klint/checks/RelativeOverlapDetector.java | 412 +++ .../checks/RequiredAttributeDetector.java | 622 ++++ .../klint/checks/ResourceCycleDetector.java | 551 ++++ .../klint/checks/ResourcePrefixDetector.java | 197 ++ .../tools/klint/checks/RtlDetector.java | 645 +++++ .../tools/klint/checks/SQLiteDetector.java | 116 + .../klint/checks/ScrollViewChildDetector.java | 101 + .../tools/klint/checks/SdCardDetector.java | 130 + .../klint/checks/SecureRandomDetector.java | 161 ++ .../checks/SecureRandomGeneratorDetector.java | 240 ++ .../tools/klint/checks/SecurityDetector.java | 411 +++ .../klint/checks/ServiceCastDetector.java | 192 ++ .../checks/SetJavaScriptEnabledDetector.java | 76 + .../klint/checks/SharedPrefsDetector.java | 325 +++ .../checks/SignatureOrSystemDetector.java | 74 + .../tools/klint/checks/StateListDetector.java | 147 + .../klint/checks/StringFormatDetector.java | 1682 +++++++++++ .../checks/SupportAnnotationDetector.java | 1398 +++++++++ .../checks/SystemPermissionsDetector.java | 189 ++ .../tools/klint/checks/TextFieldDetector.java | 344 +++ .../tools/klint/checks/TextViewDetector.java | 239 ++ .../tools/klint/checks/TitleDetector.java | 114 + .../tools/klint/checks/ToastDetector.java | 157 + .../klint/checks/TooManyViewsDetector.java | 155 + .../klint/checks/TranslationDetector.java | 722 +++++ .../tools/klint/checks/TypoDetector.java | 496 ++++ .../tools/klint/checks/TypoLookup.java | 778 +++++ .../klint/checks/TypographyDetector.java | 531 ++++ .../klint/checks/UnusedResourceDetector.java | 602 ++++ .../checks/UseCompoundDrawableDetector.java | 121 + .../klint/checks/UselessViewDetector.java | 269 ++ .../tools/klint/checks/Utf8Detector.java | 115 + .../klint/checks/ViewConstructorDetector.java | 154 + .../klint/checks/ViewHolderDetector.java | 227 ++ .../tools/klint/checks/ViewTagDetector.java | 176 ++ .../tools/klint/checks/ViewTypeDetector.java | 334 +++ .../tools/klint/checks/WakelockDetector.java | 431 +++ .../tools/klint/checks/WebViewDetector.java | 102 + .../tools/klint/checks/WrongCallDetector.java | 161 ++ .../tools/klint/checks/WrongCaseDetector.java | 117 + .../tools/klint/checks/WrongIdDetector.java | 507 ++++ .../klint/checks/WrongImportDetector.java | 112 + .../klint/checks/WrongLocationDetector.java | 70 + .../AndroidInspectionExtensionsFactory.java | 49 + .../klint/AndroidLintExternalAnnotator.java | 447 +++ .../AndroidLintGlobalInspectionContext.java | 216 ++ .../klint/AndroidLintInspectionBase.java | 531 ++++ .../AndroidLintInspectionToolProvider.java | 1798 ++++++++++++ .../klint/AndroidLintQuickFix.java | 18 + .../inspections/klint/AndroidLintUtil.java | 53 + .../klint/AndroidQuickfixContexts.java | 75 + .../inspections/klint/DomPsiConverter.java | 1302 +++++++++ .../inspections/klint/DomPsiParser.java | 183 ++ .../klint/IntellijApiDetector.java | 825 ++++++ .../inspections/klint/IntellijLintClient.java | 791 +++++ .../klint/IntellijLintIssueRegistry.java | 90 + .../klint/IntellijLintProject.java | 1043 +++++++ .../klint/IntellijLintRequest.java | 122 + .../inspections/klint/IntellijLintUtils.java | 382 +++ .../klint/IntellijRegistrationDetector.java | 130 + .../klint/IntellijViewTypeDetector.java | 49 + .../LintInspectionDescriptionLinkHandler.java | 64 + .../inspections/klint/ProblemData.java | 44 + 185 files changed, 66319 insertions(+) create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/AsmVisitor.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/CircularDependencyException.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/ClassEntry.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/CompositeIssueRegistry.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/Configuration.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultConfiguration.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultSdkInfo.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/IssueRegistry.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/JarFileIssueRegistry.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaParser.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaVisitor.java create mode 100755 plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintClient.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintDriver.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintListener.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintRequest.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/OtherFileVisitor.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/ResourceVisitor.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/SdkInfo.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/client/api/XmlParser.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Category.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ClassContext.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Context.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/DefaultPosition.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Detector.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Implementation.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Issue.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/JavaContext.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LayoutDetector.java create mode 100755 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LintUtils.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Location.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Position.java create mode 100755 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Project.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceContext.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceXmlDetector.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Scope.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Severity.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Speed.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/TextFormat.java create mode 100644 plugins/lint/lint-api/src/com/android/tools/klint/detector/api/XmlContext.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/AccessibilityDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/AddJavascriptInterfaceDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/AlarmDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/AlwaysShowActionDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/AnnotationDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/Api.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiClass.java create mode 100755 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiLookup.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ApiParser.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/AppCompatCallDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/AppCompatResourceDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/AppIndexingApiDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ArraySizeDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/AssertDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/BuiltinIssueRegistry.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ButtonDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ByteOrderMarkDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/CallSuperDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ChildCountDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/CipherGetInstanceDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/CleanupDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ClickableViewAccessibilityDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/CommentDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ControlFlowGraph.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/CustomViewDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/CutPasteDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/DateFormatDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/DeprecationDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/DetectMissingPrefix.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/DosLineEndingDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/DuplicateIdDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/DuplicateResourceDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ExtraTextDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/FieldGetterDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/FragmentDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/FullBackupContentDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/GetSignaturesDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/GradleDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/GridLayoutDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/HandlerDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/HardcodedDebugModeDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/HardcodedValuesDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/IconDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/IncludeDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/InefficientWeightDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/InvalidPackageDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/JavaPerformanceDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/JavaScriptInterfaceDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/LabelForDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/LayoutConsistencyDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/LayoutInflationDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/LocaleDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/LocaleFolderDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/LogDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ManifestDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ManifestTypoDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/MathDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/MergeRootFrameLayoutDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/MissingClassDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/MissingIdDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/NamespaceDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/NegativeMarginDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/NestedScrollingWidgetDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/NfcTechListDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/NonInternationalizedSmsDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ObsoleteLayoutParamsDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/OnClickDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/OverdrawDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/OverrideConcreteDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/OverrideDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ParcelDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/PermissionHolder.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/PermissionRequirement.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/PluralsDatabase.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/PluralsDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/PreferenceActivityDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/PrivateKeyDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/PrivateResourceDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ProguardDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/PropertyFileDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/PxUsageDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/RegistrationDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/RelativeOverlapDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/RequiredAttributeDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ResourceCycleDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ResourcePrefixDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/RtlDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/SQLiteDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ScrollViewChildDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/SdCardDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/SecureRandomDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/SecureRandomGeneratorDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/SecurityDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ServiceCastDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/SetJavaScriptEnabledDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/SharedPrefsDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/SignatureOrSystemDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/StateListDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/StringFormatDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/SupportAnnotationDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/SystemPermissionsDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/TextFieldDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/TextViewDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/TitleDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ToastDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/TooManyViewsDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/TranslationDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/TypoDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/TypoLookup.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/TypographyDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/UnusedResourceDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/UseCompoundDrawableDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/UselessViewDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/Utf8Detector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewConstructorDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewHolderDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewTagDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/ViewTypeDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/WakelockDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/WebViewDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/WrongCallDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/WrongCaseDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/WrongIdDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/WrongImportDetector.java create mode 100644 plugins/lint/lint-checks/src/com/android/tools/klint/checks/WrongLocationDetector.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidInspectionExtensionsFactory.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintExternalAnnotator.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintGlobalInspectionContext.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintInspectionBase.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintInspectionToolProvider.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintQuickFix.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidLintUtil.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/AndroidQuickfixContexts.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/DomPsiConverter.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/DomPsiParser.java create mode 100755 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijApiDetector.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintClient.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintIssueRegistry.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintProject.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintRequest.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijLintUtils.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijRegistrationDetector.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/IntellijViewTypeDetector.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/LintInspectionDescriptionLinkHandler.java create mode 100644 plugins/lint/lint-idea/src/org/jetbrains/android/inspections/klint/ProblemData.java diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/AsmVisitor.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/AsmVisitor.java new file mode 100644 index 00000000000..e8a4bb5c25b --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/AsmVisitor.java @@ -0,0 +1,202 @@ +/* + * 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.client.api; + +import com.android.annotations.NonNull; +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.google.common.annotations.Beta; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Specialized visitor for running detectors on a class object model. + *

+ * It operates in two phases: + *

    + *
  1. First, it computes a set of maps where it generates a map from each + * significant method name to a list of detectors to consult for that method + * name. The set of method names that a detector is interested in is provided + * by the detectors themselves. + *
  2. Second, it iterates over the DOM a single time. For each method call it finds, + * it dispatches to any check that has registered interest in that method name. + *
  3. Finally, it runs a full check on those class scanners that do not register + * specific method names to be checked. This is intended for those detectors + * that do custom work, not related specifically to method calls. + *
+ * It also notifies all the detectors before and after the document is processed + * such that they can do pre- and post-processing. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +class AsmVisitor { + /** + * Number of distinct node types specified in {@link AbstractInsnNode}. Sadly + * there isn't a max-constant there, so update this along with ASM library + * updates. + */ + private static final int TYPE_COUNT = AbstractInsnNode.LINE + 1; + private final Map> mMethodNameToChecks = + new HashMap>(); + private final Map> mMethodOwnerToChecks = + new HashMap>(); + private final List mFullClassChecks = new ArrayList(); + + private final List mAllDetectors; + private List[] mNodeTypeDetectors; + + // Really want this: + // & Detector.ClassScanner> ClassVisitor(T xmlDetectors) { + // but it makes client code tricky and ugly. + @SuppressWarnings("unchecked") + AsmVisitor(@NonNull LintClient client, @NonNull List classDetectors) { + mAllDetectors = classDetectors; + + // TODO: Check appliesTo() for files, and find a quick way to enable/disable + // rules when running through a full project! + for (Detector detector : classDetectors) { + Detector.ClassScanner scanner = (Detector.ClassScanner) detector; + + boolean checkFullClass = true; + + Collection names = scanner.getApplicableCallNames(); + if (names != null) { + checkFullClass = false; + for (String element : names) { + List list = mMethodNameToChecks.get(element); + if (list == null) { + list = new ArrayList(); + mMethodNameToChecks.put(element, list); + } + list.add(scanner); + } + } + + Collection owners = scanner.getApplicableCallOwners(); + if (owners != null) { + checkFullClass = false; + for (String element : owners) { + List list = mMethodOwnerToChecks.get(element); + if (list == null) { + list = new ArrayList(); + mMethodOwnerToChecks.put(element, list); + } + list.add(scanner); + } + } + + int[] types = scanner.getApplicableAsmNodeTypes(); + if (types != null) { + checkFullClass = false; + for (int type : types) { + if (type < 0 || type >= TYPE_COUNT) { + // Can't support this node type: looks like ASM wasn't updated correctly. + client.log(null, "Out of range node type %1$d from detector %2$s", + type, scanner); + continue; + } + if (mNodeTypeDetectors == null) { + mNodeTypeDetectors = new List[TYPE_COUNT]; + } + List checks = mNodeTypeDetectors[type]; + if (checks == null) { + checks = new ArrayList(); + mNodeTypeDetectors[type] = checks; + } + checks.add(scanner); + } + } + + if (checkFullClass) { + mFullClassChecks.add(detector); + } + } + } + + @SuppressWarnings("rawtypes") // ASM API uses raw types + void runClassDetectors(ClassContext context) { + ClassNode classNode = context.getClassNode(); + + for (Detector detector : mAllDetectors) { + detector.beforeCheckFile(context); + } + + for (Detector detector : mFullClassChecks) { + Detector.ClassScanner scanner = (Detector.ClassScanner) detector; + scanner.checkClass(context, classNode); + detector.afterCheckFile(context); + } + + if (!mMethodNameToChecks.isEmpty() || !mMethodOwnerToChecks.isEmpty() || + mNodeTypeDetectors != null && mNodeTypeDetectors.length > 0) { + List methodList = classNode.methods; + for (Object m : methodList) { + MethodNode method = (MethodNode) m; + InsnList nodes = method.instructions; + for (int i = 0, n = nodes.size(); i < n; i++) { + AbstractInsnNode instruction = nodes.get(i); + int type = instruction.getType(); + if (type == AbstractInsnNode.METHOD_INSN) { + MethodInsnNode call = (MethodInsnNode) instruction; + + String owner = call.owner; + List scanners = mMethodOwnerToChecks.get(owner); + if (scanners != null) { + for (ClassScanner scanner : scanners) { + scanner.checkCall(context, classNode, method, call); + } + } + + String name = call.name; + scanners = mMethodNameToChecks.get(name); + if (scanners != null) { + for (ClassScanner scanner : scanners) { + scanner.checkCall(context, classNode, method, call); + } + } + } + + if (mNodeTypeDetectors != null && type < mNodeTypeDetectors.length) { + List scanners = mNodeTypeDetectors[type]; + if (scanners != null) { + for (ClassScanner scanner : scanners) { + scanner.checkInstruction(context, classNode, method, instruction); + } + } + } + } + } + } + + for (Detector detector : mAllDetectors) { + detector.afterCheckFile(context); + } + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/CircularDependencyException.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/CircularDependencyException.java new file mode 100644 index 00000000000..337eb279dfe --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/CircularDependencyException.java @@ -0,0 +1,81 @@ +/* + * 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.client.api; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.lint.detector.api.Location; +import com.android.tools.lint.detector.api.Project; +import com.google.common.annotations.Beta; + +/** + * Exception thrown when there is a circular dependency, such as a circular dependency + * of library mProject references + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public class CircularDependencyException extends RuntimeException { + @Nullable + private Project mProject; + + @Nullable + private Location mLocation; + + CircularDependencyException(@NonNull String message) { + super(message); + } + + /** + * Returns the associated project, if any + * + * @return the associated project, if any + */ + @Nullable + public Project getProject() { + return mProject; + } + + /** + * Sets the associated project, if any + * + * @param project the associated project, if any + */ + public void setProject(@Nullable Project project) { + mProject = project; + } + + /** + * Returns the associated location, if any + * + * @return the associated location, if any + */ + @Nullable + public Location getLocation() { + return mLocation; + } + + /** + * Sets the associated location, if any + * + * @param location the associated location, if any + */ + public void setLocation(@Nullable Location location) { + mLocation = location; + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/ClassEntry.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/ClassEntry.java new file mode 100644 index 00000000000..c4d3eb5eb5d --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/ClassEntry.java @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2015 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.client.api; + +import static com.android.SdkConstants.DOT_CLASS; +import static com.android.SdkConstants.DOT_JAR; +import static org.objectweb.asm.Opcodes.ASM5; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.google.common.collect.Maps; +import com.google.common.io.ByteStreams; +import com.google.common.io.Closeables; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** A class, present either as a .class file on disk, or inside a .jar file. */ +@VisibleForTesting +class ClassEntry implements Comparable { + public final File file; + public final File jarFile; + public final File binDir; + public final byte[] bytes; + + @VisibleForTesting + ClassEntry( + @NonNull File file, + @Nullable File jarFile, + @NonNull File binDir, + @NonNull byte[] bytes) { + super(); + this.file = file; + this.jarFile = jarFile; + this.binDir = binDir; + this.bytes = bytes; + } + + @NonNull + public String path() { + if (jarFile != null) { + return jarFile.getPath() + ':' + file.getPath(); + } else { + return file.getPath(); + } + } + + @Override + public int compareTo(@NonNull ClassEntry other) { + String p1 = file.getPath(); + String p2 = other.file.getPath(); + int m1 = p1.length(); + int m2 = p2.length(); + if (m1 == m2 && p1.equals(p2)) { + return 0; + } + int m = Math.min(m1, m2); + + for (int i = 0; i < m; i++) { + char c1 = p1.charAt(i); + char c2 = p2.charAt(i); + if (c1 != c2) { + // Sort Foo$Bar.class *after* Foo.class, even though $ < . + if (c1 == '.' && c2 == '$') { + return -1; + } + if (c1 == '$' && c2 == '.') { + return 1; + } + return c1 - c2; + } + } + + return (m == m1) ? -1 : 1; + } + + @Override + public String toString() { + return file.getPath(); + } + + /** + * Creates a list of class entries from the given class path. + * + * @param client the client to report errors to and to use to read files + * @param classPath the class path (directories and jar files) to scan + * @param sort if true, sort the results + * @return the list of class entries, never null. + */ + @NonNull + public static List fromClassPath( + @NonNull LintClient client, + @NonNull List classPath, + boolean sort) { + if (!classPath.isEmpty()) { + List libraryEntries = new ArrayList(64); + addEntries(client, libraryEntries, classPath); + if (sort) { + Collections.sort(libraryEntries); + } + return libraryEntries; + } else { + return Collections.emptyList(); + } + } + + /** + * Creates a list of class entries from the given class path and specific set of + * files within it. + * + * @param client the client to report errors to and to use to read files + * @param classFiles the specific set of class files to look for + * @param classFolders the list of class folders to look in (to determine the + * package root) + * @param sort if true, sort the results + * @return the list of class entries, never null. + */ + @NonNull + public static List fromClassFiles( + @NonNull LintClient client, + @NonNull List classFiles, @NonNull List classFolders, + boolean sort) { + List entries = new ArrayList(classFiles.size()); + + if (!classFolders.isEmpty()) { + for (File file : classFiles) { + String path = file.getPath(); + if (file.isFile() && path.endsWith(DOT_CLASS)) { + try { + byte[] bytes = client.readBytes(file); + for (File dir : classFolders) { + if (path.startsWith(dir.getPath())) { + entries.add(new ClassEntry(file, null /* jarFile*/, dir, + bytes)); + break; + } + } + } catch (IOException e) { + client.log(e, null); + } + } + } + + if (sort && !entries.isEmpty()) { + Collections.sort(entries); + } + } + + return entries; + } + + /** + * Given a classpath, add all the class files found within the directories and inside jar files + */ + private static void addEntries( + @NonNull LintClient client, + @NonNull List entries, + @NonNull List classPath) { + for (File classPathEntry : classPath) { + if (classPathEntry.getName().endsWith(DOT_JAR)) { + //noinspection UnnecessaryLocalVariable + File jarFile = classPathEntry; + if (!jarFile.exists()) { + continue; + } + ZipInputStream zis = null; + try { + FileInputStream fis = new FileInputStream(jarFile); + try { + zis = new ZipInputStream(fis); + ZipEntry entry = zis.getNextEntry(); + while (entry != null) { + String name = entry.getName(); + if (name.endsWith(DOT_CLASS)) { + try { + byte[] bytes = ByteStreams.toByteArray(zis); + if (bytes != null) { + File file = new File(entry.getName()); + entries.add(new ClassEntry(file, jarFile, jarFile, bytes)); + } + } catch (Exception e) { + client.log(e, null); + continue; + } + } + + entry = zis.getNextEntry(); + } + } finally { + Closeables.close(fis, true); + } + } catch (IOException e) { + client.log(e, "Could not read jar file contents from %1$s", jarFile); + } finally { + try { + Closeables.close(zis, true); + } catch (IOException e) { + // cannot happen + } + } + } else if (classPathEntry.isDirectory()) { + //noinspection UnnecessaryLocalVariable + File binDir = classPathEntry; + List classFiles = new ArrayList(); + addClassFiles(binDir, classFiles); + + for (File file : classFiles) { + try { + byte[] bytes = client.readBytes(file); + entries.add(new ClassEntry(file, null /* jarFile*/, binDir, bytes)); + } catch (IOException e) { + client.log(e, null); + } + } + } else { + client.log(null, "Ignoring class path entry %1$s", classPathEntry); + } + } + } + + /** Adds in all the .class files found recursively in the given directory */ + private static void addClassFiles(@NonNull File dir, @NonNull List classFiles) { + // Process the resource folder + File[] files = dir.listFiles(); + if (files != null && files.length > 0) { + for (File file : files) { + if (file.isFile() && file.getName().endsWith(DOT_CLASS)) { + classFiles.add(file); + } else if (file.isDirectory()) { + // Recurse + addClassFiles(file, classFiles); + } + } + } + } + + /** + * Creates a super class map (from class to its super class) for the given set of entries + * + * @param client the client to report errors to and to use to access files + * @param libraryEntries the set of library entries to consult + * @param classEntries the set of class entries to consult + * @return a map from name to super class internal names + */ + @NonNull + public static Map createSuperClassMap( + @NonNull LintClient client, + @NonNull List libraryEntries, + @NonNull List classEntries) { + int size = libraryEntries.size() + classEntries.size(); + Map map = Maps.newHashMapWithExpectedSize(size); + SuperclassVisitor visitor = new SuperclassVisitor(map); + addSuperClasses(client, visitor, libraryEntries); + addSuperClasses(client, visitor, classEntries); + return map; + } + + /** + * Creates a super class map (from class to its super class) for the given set of entries + * + * @param client the client to report errors to and to use to access files + * @param entries the set of library entries to consult + * @return a map from name to super class internal names + */ + @NonNull + public static Map createSuperClassMap( + @NonNull LintClient client, + @NonNull List entries) { + Map map = Maps.newHashMapWithExpectedSize(entries.size()); + SuperclassVisitor visitor = new SuperclassVisitor(map); + addSuperClasses(client, visitor, entries); + return map; + } + + /** Adds in all the super classes found for the given class entries into the given map */ + private static void addSuperClasses( + @NonNull LintClient client, + @NonNull SuperclassVisitor visitor, + @NonNull List entries) { + for (ClassEntry entry : entries) { + try { + ClassReader reader = new ClassReader(entry.bytes); + int flags = ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG + | ClassReader.SKIP_FRAMES; + reader.accept(visitor, flags); + } catch (Throwable t) { + client.log(null, "Error processing %1$s: broken class file?", entry.path()); + } + } + } + + /** Visitor skimming classes and initializing a map of super classes */ + private static class SuperclassVisitor extends ClassVisitor { + private final Map mMap; + + public SuperclassVisitor(Map map) { + super(ASM5); + mMap = map; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, + String[] interfaces) { + // Record super class in the map (but don't waste space on java.lang.Object) + if (superName != null && !"java/lang/Object".equals(superName)) { + mMap.put(name, superName); + } + } + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/CompositeIssueRegistry.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/CompositeIssueRegistry.java new file mode 100644 index 00000000000..70b6a09b74a --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/CompositeIssueRegistry.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2013 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.client.api; + +import com.android.annotations.NonNull; +import com.android.tools.lint.detector.api.Issue; +import com.google.common.collect.Lists; + +import java.util.List; + +/** + * Registry which merges many issue registries into one, and presents a unified list + * of issues. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +class CompositeIssueRegistry extends IssueRegistry { + private final List myRegistries; + private List myIssues; + + public CompositeIssueRegistry(@NonNull List registries) { + myRegistries = registries; + } + + @NonNull + @Override + public List getIssues() { + if (myIssues == null) { + List issues = Lists.newArrayListWithExpectedSize(200); + for (IssueRegistry registry : myRegistries) { + issues.addAll(registry.getIssues()); + } + myIssues = issues; + } + + return myIssues; + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/Configuration.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/Configuration.java new file mode 100644 index 00000000000..64334a4eba5 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/Configuration.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2011 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.client.api; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +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 com.google.common.annotations.Beta; + +/** + * Lint configuration for an Android project such as which specific rules to include, + * which specific rules to exclude, and which specific errors to ignore. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public abstract class Configuration { + /** + * Checks whether this issue should be ignored because the user has already + * suppressed the error? Note that this refers to individual issues being + * suppressed/ignored, not a whole detector being disabled via something + * like {@link #isEnabled(Issue)}. + * + * @param context the context used by the detector when the issue was found + * @param issue the issue that was found + * @param location the location of the issue + * @param message the associated user message + * @return true if this issue should be suppressed + */ + public boolean isIgnored( + @NonNull Context context, + @NonNull Issue issue, + @Nullable Location location, + @NonNull String message) { + return false; + } + + /** + * Returns false if the given issue has been disabled. This is just + * a convenience method for {@code getSeverity(issue) != Severity.IGNORE}. + * + * @param issue the issue to check + * @return false if the issue has been disabled + */ + public boolean isEnabled(@NonNull Issue issue) { + return getSeverity(issue) != Severity.IGNORE; + } + + /** + * Returns the severity for a given issue. This is the same as the + * {@link Issue#getDefaultSeverity()} unless the user has selected a custom + * severity (which is tool context dependent). + * + * @param issue the issue to look up the severity from + * @return the severity use for issues for the given detector + */ + public Severity getSeverity(@NonNull Issue issue) { + return issue.getDefaultSeverity(); + } + + // Editing configurations + + /** + * Marks the given warning as "ignored". + * + * @param context The scanning context + * @param issue the issue to be ignored + * @param location The location to ignore the warning at, if any + * @param message The message for the warning + */ + public abstract void ignore( + @NonNull Context context, + @NonNull Issue issue, + @Nullable Location location, + @NonNull String message); + + /** + * Sets the severity to be used for this issue. + * + * @param issue the issue to set the severity for + * @param severity the severity to associate with this issue, or null to + * reset the severity to the default + */ + public abstract void setSeverity(@NonNull Issue issue, @Nullable Severity severity); + + // Bulk editing support + + /** + * Marks the beginning of a "bulk" editing operation with repeated calls to + * {@link #setSeverity} or {@link #ignore}. After all the values have been + * set, the client must call {@link #finishBulkEditing()}. This + * allows configurations to avoid doing expensive I/O (such as writing out a + * config XML file) for each and every editing operation when they are + * applied in bulk, such as from a configuration dialog's "Apply" action. + */ + public void startBulkEditing() { + } + + /** + * Marks the end of a "bulk" editing operation, where values should be + * committed to persistent storage. See {@link #startBulkEditing()} for + * details. + */ + public void finishBulkEditing() { + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultConfiguration.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultConfiguration.java new file mode 100644 index 00000000000..ca7e76e7ccc --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultConfiguration.java @@ -0,0 +1,609 @@ +/* + * Copyright (C) 2011 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.client.api; + +import static com.android.SdkConstants.CURRENT_PLATFORM; +import static com.android.SdkConstants.PLATFORM_WINDOWS; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +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.Project; +import com.android.tools.lint.detector.api.Severity; +import com.android.tools.lint.detector.api.TextFormat; +import com.android.utils.XmlUtils; +import com.google.common.annotations.Beta; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Splitter; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXParseException; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * Default implementation of a {@link Configuration} which reads and writes + * configuration data into {@code lint.xml} in the project directory. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public class DefaultConfiguration extends Configuration { + private final LintClient mClient; + /** Default name of the configuration file */ + public static final String CONFIG_FILE_NAME = "lint.xml"; //$NON-NLS-1$ + + // Lint XML File + @NonNull + private static final String TAG_ISSUE = "issue"; //$NON-NLS-1$ + @NonNull + private static final String ATTR_ID = "id"; //$NON-NLS-1$ + @NonNull + private static final String ATTR_SEVERITY = "severity"; //$NON-NLS-1$ + @NonNull + private static final String ATTR_PATH = "path"; //$NON-NLS-1$ + @NonNull + private static final String ATTR_REGEXP = "regexp"; //$NON-NLS-1$ + @NonNull + private static final String TAG_IGNORE = "ignore"; //$NON-NLS-1$ + @NonNull + private static final String VALUE_ALL = "all"; //$NON-NLS-1$ + + private final Configuration mParent; + private final Project mProject; + private final File mConfigFile; + private boolean mBulkEditing; + + /** Map from id to list of project-relative paths for suppressed warnings */ + private Map> mSuppressed; + + /** Map from id to regular expressions. */ + @Nullable + private Map> mRegexps; + + /** + * Map from id to custom {@link Severity} override + */ + private Map mSeverity; + + protected DefaultConfiguration( + @NonNull LintClient client, + @Nullable Project project, + @Nullable Configuration parent, + @NonNull File configFile) { + mClient = client; + mProject = project; + mParent = parent; + mConfigFile = configFile; + } + + protected DefaultConfiguration( + @NonNull LintClient client, + @NonNull Project project, + @Nullable Configuration parent) { + this(client, project, parent, new File(project.getDir(), CONFIG_FILE_NAME)); + } + + /** + * Creates a new {@link DefaultConfiguration} + * + * @param client the client to report errors to etc + * @param project the associated project + * @param parent the parent/fallback configuration or null + * @return a new configuration + */ + @NonNull + public static DefaultConfiguration create( + @NonNull LintClient client, + @NonNull Project project, + @Nullable Configuration parent) { + return new DefaultConfiguration(client, project, parent); + } + + /** + * Creates a new {@link DefaultConfiguration} for the given lint config + * file, not affiliated with a project. This is used for global + * configurations. + * + * @param client the client to report errors to etc + * @param lintFile the lint file containing the configuration + * @return a new configuration + */ + @NonNull + public static DefaultConfiguration create(@NonNull LintClient client, @NonNull File lintFile) { + return new DefaultConfiguration(client, null /*project*/, null /*parent*/, lintFile); + } + + @Override + public boolean isIgnored( + @NonNull Context context, + @NonNull Issue issue, + @Nullable Location location, + @NonNull String message) { + ensureInitialized(); + + String id = issue.getId(); + List paths = mSuppressed.get(id); + if (paths == null) { + paths = mSuppressed.get(VALUE_ALL); + } + if (paths != null && location != null) { + File file = location.getFile(); + String relativePath = context.getProject().getRelativePath(file); + for (String suppressedPath : paths) { + if (suppressedPath.equals(relativePath)) { + return true; + } + // Also allow a prefix + if (relativePath.startsWith(suppressedPath)) { + return true; + } + } + } + + if (mRegexps != null) { + List regexps = mRegexps.get(id); + if (regexps == null) { + regexps = mRegexps.get(VALUE_ALL); + } + if (regexps != null && location != null) { + // Check message + for (Pattern regexp : regexps) { + Matcher matcher = regexp.matcher(message); + if (matcher.find()) { + return true; + } + } + + // Check location + File file = location.getFile(); + String relativePath = context.getProject().getRelativePath(file); + boolean checkUnixPath = false; + for (Pattern regexp : regexps) { + Matcher matcher = regexp.matcher(relativePath); + if (matcher.find()) { + return true; + } else if (regexp.pattern().indexOf('/') != -1) { + checkUnixPath = true; + } + } + + if (checkUnixPath && CURRENT_PLATFORM == PLATFORM_WINDOWS) { + relativePath = relativePath.replace('\\', '/'); + for (Pattern regexp : regexps) { + Matcher matcher = regexp.matcher(relativePath); + if (matcher.find()) { + return true; + } + } + } + } + } + + return mParent != null && mParent.isIgnored(context, issue, location, message); + } + + @NonNull + protected Severity getDefaultSeverity(@NonNull Issue issue) { + if (!issue.isEnabledByDefault()) { + return Severity.IGNORE; + } + + return issue.getDefaultSeverity(); + } + + @Override + @NonNull + public Severity getSeverity(@NonNull Issue issue) { + ensureInitialized(); + + Severity severity = mSeverity.get(issue.getId()); + if (severity == null) { + severity = mSeverity.get(VALUE_ALL); + } + + if (severity != null) { + return severity; + } + + if (mParent != null) { + return mParent.getSeverity(issue); + } + + return getDefaultSeverity(issue); + } + + private void ensureInitialized() { + if (mSuppressed == null) { + readConfig(); + } + } + + private void formatError(String message, Object... args) { + if (args != null && args.length > 0) { + message = String.format(message, args); + } + message = "Failed to parse `lint.xml` configuration file: " + message; + LintDriver driver = new LintDriver(new IssueRegistry() { + @Override @NonNull public List getIssues() { + return Collections.emptyList(); + } + }, mClient); + mClient.report(new Context(driver, mProject, mProject, mConfigFile), + IssueRegistry.LINT_ERROR, + mProject.getConfiguration().getSeverity(IssueRegistry.LINT_ERROR), + Location.create(mConfigFile), message, TextFormat.RAW); + } + + private void readConfig() { + mSuppressed = new HashMap>(); + mSeverity = new HashMap(); + + if (!mConfigFile.exists()) { + return; + } + + try { + Document document = XmlUtils.parseUtfXmlFile(mConfigFile, false); + NodeList issues = document.getElementsByTagName(TAG_ISSUE); + Splitter splitter = Splitter.on(',').trimResults().omitEmptyStrings(); + for (int i = 0, count = issues.getLength(); i < count; i++) { + Node node = issues.item(i); + Element element = (Element) node; + String idList = element.getAttribute(ATTR_ID); + if (idList.isEmpty()) { + formatError("Invalid lint config file: Missing required issue id attribute"); + continue; + } + Iterable ids = splitter.split(idList); + + NamedNodeMap attributes = node.getAttributes(); + for (int j = 0, n = attributes.getLength(); j < n; j++) { + Node attribute = attributes.item(j); + String name = attribute.getNodeName(); + String value = attribute.getNodeValue(); + if (ATTR_ID.equals(name)) { + // already handled + } else if (ATTR_SEVERITY.equals(name)) { + for (Severity severity : Severity.values()) { + if (value.equalsIgnoreCase(severity.name())) { + for (String id : ids) { + mSeverity.put(id, severity); + } + break; + } + } + } else { + formatError("Unexpected attribute \"%1$s\"", name); + } + } + + // Look up ignored errors + NodeList childNodes = element.getChildNodes(); + if (childNodes.getLength() > 0) { + for (int j = 0, n = childNodes.getLength(); j < n; j++) { + Node child = childNodes.item(j); + if (child.getNodeType() == Node.ELEMENT_NODE) { + Element ignore = (Element) child; + String path = ignore.getAttribute(ATTR_PATH); + if (path.isEmpty()) { + String regexp = ignore.getAttribute(ATTR_REGEXP); + if (regexp.isEmpty()) { + formatError("Missing required attribute %1$s or %2$s under %3$s", + ATTR_PATH, ATTR_REGEXP, idList); + } else { + addRegexp(idList, ids, n, regexp, false); + } + } else { + // Normalize path format to File.separator. Also + // handle the file format containing / or \. + if (File.separatorChar == '/') { + path = path.replace('\\', '/'); + } else { + path = path.replace('/', File.separatorChar); + } + + if (path.indexOf('*') != -1) { + String regexp = globToRegexp(path); + addRegexp(idList, ids, n, regexp, false); + } else { + for (String id : ids) { + List paths = mSuppressed.get(id); + if (paths == null) { + paths = new ArrayList(n / 2 + 1); + mSuppressed.put(id, paths); + } + paths.add(path); + } + } + } + } + } + } + } + } catch (SAXParseException e) { + formatError(e.getMessage()); + } catch (Exception e) { + mClient.log(e, null); + } + } + + @VisibleForTesting + @NonNull + public static String globToRegexp(@NonNull String glob) { + StringBuilder sb = new StringBuilder(glob.length() * 2); + int begin = 0; + sb.append('^'); + for (int i = 0, n = glob.length(); i < n; i++) { + char c = glob.charAt(i); + if (c == '*') { + begin = appendQuoted(sb, glob, begin, i) + 1; + if (i < n - 1 && glob.charAt(i + 1) == '*') { + i++; + begin++; + } + sb.append(".*?"); + } else if (c == '?') { + begin = appendQuoted(sb, glob, begin, i) + 1; + sb.append(".?"); + } + } + appendQuoted(sb, glob, begin, glob.length()); + sb.append('$'); + return sb.toString(); + } + + private static int appendQuoted(StringBuilder sb, String s, int from, int to) { + if (to > from) { + boolean isSimple = true; + for (int i = from; i < to; i++) { + char c = s.charAt(i); + if (!Character.isLetterOrDigit(c) && c != '/' && c != ' ') { + isSimple = false; + break; + } + } + if (isSimple) { + for (int i = from; i < to; i++) { + sb.append(s.charAt(i)); + } + return to; + } + sb.append(Pattern.quote(s.substring(from, to))); + } + return to; + } + + private void addRegexp(@NonNull String idList, @NonNull Iterable ids, int n, + @NonNull String regexp, boolean silent) { + try { + if (mRegexps == null) { + mRegexps = new HashMap>(); + } + Pattern pattern = Pattern.compile(regexp); + for (String id : ids) { + List paths = mRegexps.get(id); + if (paths == null) { + paths = new ArrayList(n / 2 + 1); + mRegexps.put(id, paths); + } + paths.add(pattern); + } + } catch (PatternSyntaxException e) { + if (!silent) { + formatError("Invalid pattern %1$s under %2$s: %3$s", + regexp, idList, e.getDescription()); + } + } + } + + private void writeConfig() { + try { + // Write the contents to a new file first such that we don't clobber the + // existing file if some I/O error occurs. + File file = new File(mConfigFile.getParentFile(), + mConfigFile.getName() + ".new"); //$NON-NLS-1$ + + Writer writer = new BufferedWriter(new FileWriter(file)); + writer.write( + "\n" + //$NON-NLS-1$ + "\n"); //$NON-NLS-1$ + + if (!mSuppressed.isEmpty() || !mSeverity.isEmpty()) { + // Process the maps in a stable sorted order such that if the + // files are checked into version control with the project, + // there are no random diffs just because hashing algorithms + // differ: + Set idSet = new HashSet(); + for (String id : mSuppressed.keySet()) { + idSet.add(id); + } + for (String id : mSeverity.keySet()) { + idSet.add(id); + } + List ids = new ArrayList(idSet); + Collections.sort(ids); + + for (String id : ids) { + writer.write(" <"); //$NON-NLS-1$ + writer.write(TAG_ISSUE); + writeAttribute(writer, ATTR_ID, id); + Severity severity = mSeverity.get(id); + if (severity != null) { + writeAttribute(writer, ATTR_SEVERITY, + severity.name().toLowerCase(Locale.US)); + } + + List regexps = mRegexps != null ? mRegexps.get(id) : null; + List paths = mSuppressed.get(id); + if (paths != null && !paths.isEmpty() + || regexps != null && !regexps.isEmpty()) { + writer.write('>'); + writer.write('\n'); + // The paths are already kept in sorted order when they are modified + // by ignore(...) + if (paths != null) { + for (String path : paths) { + writer.write(" <"); //$NON-NLS-1$ + writer.write(TAG_IGNORE); + writeAttribute(writer, ATTR_PATH, path.replace('\\', '/')); + writer.write(" />\n"); //$NON-NLS-1$ + } + } + if (regexps != null) { + for (Pattern regexp : regexps) { + writer.write(" <"); //$NON-NLS-1$ + writer.write(TAG_IGNORE); + writeAttribute(writer, ATTR_REGEXP, regexp.pattern()); + writer.write(" />\n"); //$NON-NLS-1$ + } + } + writer.write(" '); + writer.write('\n'); + } else { + writer.write(" />\n"); //$NON-NLS-1$ + } + } + } + + writer.write(""); //$NON-NLS-1$ + writer.close(); + + // Move file into place: move current version to lint.xml~ (removing the old ~ file + // if it exists), then move the new version to lint.xml. + File oldFile = new File(mConfigFile.getParentFile(), + mConfigFile.getName() + '~'); //$NON-NLS-1$ + if (oldFile.exists()) { + oldFile.delete(); + } + if (mConfigFile.exists()) { + mConfigFile.renameTo(oldFile); + } + boolean ok = file.renameTo(mConfigFile); + if (ok && oldFile.exists()) { + oldFile.delete(); + } + } catch (Exception e) { + mClient.log(e, null); + } + } + + private static void writeAttribute( + @NonNull Writer writer, @NonNull String name, @NonNull String value) + throws IOException { + writer.write(' '); + writer.write(name); + writer.write('='); + writer.write('"'); + writer.write(value); + writer.write('"'); + } + + @Override + public void ignore( + @NonNull Context context, + @NonNull Issue issue, + @Nullable Location location, + @NonNull String message) { + // This configuration only supports suppressing warnings on a per-file basis + if (location != null) { + ignore(issue, location.getFile()); + } + } + + /** + * Marks the given issue and file combination as being ignored. + * + * @param issue the issue to be ignored in the given file + * @param file the file to ignore the issue in + */ + public void ignore(@NonNull Issue issue, @NonNull File file) { + ensureInitialized(); + + String path = mProject != null ? mProject.getRelativePath(file) : file.getPath(); + + List paths = mSuppressed.get(issue.getId()); + if (paths == null) { + paths = new ArrayList(); + mSuppressed.put(issue.getId(), paths); + } + paths.add(path); + + // Keep paths sorted alphabetically; makes XML output stable + Collections.sort(paths); + + if (!mBulkEditing) { + writeConfig(); + } + } + + @Override + public void setSeverity(@NonNull Issue issue, @Nullable Severity severity) { + ensureInitialized(); + + String id = issue.getId(); + if (severity == null) { + mSeverity.remove(id); + } else { + mSeverity.put(id, severity); + } + + if (!mBulkEditing) { + writeConfig(); + } + } + + @Override + public void startBulkEditing() { + mBulkEditing = true; + } + + @Override + public void finishBulkEditing() { + mBulkEditing = false; + writeConfig(); + } + + @VisibleForTesting + File getConfigFile() { + return mConfigFile; + } +} \ No newline at end of file diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultSdkInfo.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultSdkInfo.java new file mode 100644 index 00000000000..2f546594ce6 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/DefaultSdkInfo.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2011 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.client.api; + +import static com.android.SdkConstants.ABSOLUTE_LAYOUT; +import static com.android.SdkConstants.ABS_LIST_VIEW; +import static com.android.SdkConstants.ABS_SEEK_BAR; +import static com.android.SdkConstants.ABS_SPINNER; +import static com.android.SdkConstants.ADAPTER_VIEW; +import static com.android.SdkConstants.AUTO_COMPLETE_TEXT_VIEW; +import static com.android.SdkConstants.BUTTON; +import static com.android.SdkConstants.CHECKABLE; +import static com.android.SdkConstants.CHECKED_TEXT_VIEW; +import static com.android.SdkConstants.CHECK_BOX; +import static com.android.SdkConstants.COMPOUND_BUTTON; +import static com.android.SdkConstants.EDIT_TEXT; +import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW; +import static com.android.SdkConstants.FRAME_LAYOUT; +import static com.android.SdkConstants.GALLERY; +import static com.android.SdkConstants.GRID_VIEW; +import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW; +import static com.android.SdkConstants.IMAGE_BUTTON; +import static com.android.SdkConstants.IMAGE_VIEW; +import static com.android.SdkConstants.LINEAR_LAYOUT; +import static com.android.SdkConstants.LIST_VIEW; +import static com.android.SdkConstants.MULTI_AUTO_COMPLETE_TEXT_VIEW; +import static com.android.SdkConstants.PROGRESS_BAR; +import static com.android.SdkConstants.RADIO_BUTTON; +import static com.android.SdkConstants.RADIO_GROUP; +import static com.android.SdkConstants.RELATIVE_LAYOUT; +import static com.android.SdkConstants.SCROLL_VIEW; +import static com.android.SdkConstants.SEEK_BAR; +import static com.android.SdkConstants.SPINNER; +import static com.android.SdkConstants.SURFACE_VIEW; +import static com.android.SdkConstants.SWITCH; +import static com.android.SdkConstants.TABLE_LAYOUT; +import static com.android.SdkConstants.TABLE_ROW; +import static com.android.SdkConstants.TAB_HOST; +import static com.android.SdkConstants.TAB_WIDGET; +import static com.android.SdkConstants.TEXT_VIEW; +import static com.android.SdkConstants.TOGGLE_BUTTON; +import static com.android.SdkConstants.VIEW; +import static com.android.SdkConstants.VIEW_ANIMATOR; +import static com.android.SdkConstants.VIEW_GROUP; +import static com.android.SdkConstants.VIEW_PKG_PREFIX; +import static com.android.SdkConstants.VIEW_STUB; +import static com.android.SdkConstants.VIEW_SWITCHER; +import static com.android.SdkConstants.WEB_VIEW; +import static com.android.SdkConstants.WIDGET_PKG_PREFIX; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.google.common.annotations.Beta; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Default simple implementation of an {@link SdkInfo} + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +class DefaultSdkInfo extends SdkInfo { + @Override + @Nullable + public String getParentViewName(@NonNull String name) { + name = getRawType(name); + + return PARENTS.get(name); + } + + @Override + @Nullable + public String getParentViewClass(@NonNull String fqcn) { + int index = fqcn.lastIndexOf('.'); + if (index != -1) { + fqcn = fqcn.substring(index + 1); + } + + String parent = PARENTS.get(fqcn); + if (parent == null) { + return null; + } + // The map only stores class names internally; correct for full package paths + if (parent.equals(VIEW) || parent.equals(VIEW_GROUP) || parent.equals(SURFACE_VIEW)) { + return VIEW_PKG_PREFIX + parent; + } else { + return WIDGET_PKG_PREFIX + parent; + } + } + + @Override + public boolean isSubViewOf(@NonNull String parentType, @NonNull String childType) { + String parent = getRawType(parentType); + String child = getRawType(childType); + + // Do analysis just on non-fqcn paths + if (parent.indexOf('.') != -1) { + parent = parent.substring(parent.lastIndexOf('.') + 1); + } + if (child.indexOf('.') != -1) { + child = child.substring(child.lastIndexOf('.') + 1); + } + + if (parent.equals(VIEW)) { + return true; + } + + while (!child.equals(VIEW)) { + if (parent.equals(child)) { + return true; + } + if (implementsInterface(child, parentType)) { + return true; + } + child = PARENTS.get(child); + if (child == null) { + // Unknown view - err on the side of caution + return true; + } + } + + return false; + } + + private static boolean implementsInterface(String className, String interfaceName) { + return interfaceName.equals(INTERFACES.get(className)); + } + + // Strip off type parameters, e.g. AdapterView => AdapterView + private static String getRawType(String type) { + if (type != null) { + int index = type.indexOf('<'); + if (index != -1) { + type = type.substring(0, index); + } + } + + return type; + } + + @Override + public boolean isLayout(@NonNull String tag) { + // TODO: Read in widgets.txt from the platform install area to look up this information + // dynamically instead! + + if (super.isLayout(tag)) { + return true; + } + + return LAYOUTS.contains(tag); + } + + private static final int CLASS_COUNT = 59; + private static final int LAYOUT_COUNT = 20; + + private static final Map PARENTS = Maps.newHashMapWithExpectedSize(CLASS_COUNT); + private static final Set LAYOUTS = Sets.newHashSetWithExpectedSize(CLASS_COUNT); + + static { + PARENTS.put(COMPOUND_BUTTON, BUTTON); + PARENTS.put(ABS_SPINNER, ADAPTER_VIEW); + PARENTS.put(ABS_LIST_VIEW, ADAPTER_VIEW); + PARENTS.put(ABS_SEEK_BAR, ADAPTER_VIEW); + PARENTS.put(ADAPTER_VIEW, VIEW_GROUP); + PARENTS.put(VIEW_GROUP, VIEW); + + PARENTS.put(TEXT_VIEW, VIEW); + PARENTS.put(CHECKED_TEXT_VIEW, TEXT_VIEW); + PARENTS.put(RADIO_BUTTON, COMPOUND_BUTTON); + PARENTS.put(SPINNER, ABS_SPINNER); + PARENTS.put(IMAGE_BUTTON, IMAGE_VIEW); + PARENTS.put(IMAGE_VIEW, VIEW); + PARENTS.put(EDIT_TEXT, TEXT_VIEW); + PARENTS.put(PROGRESS_BAR, VIEW); + PARENTS.put(TOGGLE_BUTTON, COMPOUND_BUTTON); + PARENTS.put(VIEW_STUB, VIEW); + PARENTS.put(BUTTON, TEXT_VIEW); + PARENTS.put(SEEK_BAR, ABS_SEEK_BAR); + PARENTS.put(CHECK_BOX, COMPOUND_BUTTON); + PARENTS.put(SWITCH, COMPOUND_BUTTON); + PARENTS.put(GALLERY, ABS_SPINNER); + PARENTS.put(SURFACE_VIEW, VIEW); + PARENTS.put(ABSOLUTE_LAYOUT, VIEW_GROUP); + PARENTS.put(LINEAR_LAYOUT, VIEW_GROUP); + PARENTS.put(RELATIVE_LAYOUT, VIEW_GROUP); + PARENTS.put(LIST_VIEW, ABS_LIST_VIEW); + PARENTS.put(VIEW_SWITCHER, VIEW_ANIMATOR); + PARENTS.put(FRAME_LAYOUT, VIEW_GROUP); + PARENTS.put(HORIZONTAL_SCROLL_VIEW, FRAME_LAYOUT); + PARENTS.put(VIEW_ANIMATOR, FRAME_LAYOUT); + PARENTS.put(TAB_HOST, FRAME_LAYOUT); + PARENTS.put(TABLE_ROW, LINEAR_LAYOUT); + PARENTS.put(RADIO_GROUP, LINEAR_LAYOUT); + PARENTS.put(TAB_WIDGET, LINEAR_LAYOUT); + PARENTS.put(EXPANDABLE_LIST_VIEW, LIST_VIEW); + PARENTS.put(TABLE_LAYOUT, LINEAR_LAYOUT); + PARENTS.put(SCROLL_VIEW, FRAME_LAYOUT); + PARENTS.put(GRID_VIEW, ABS_LIST_VIEW); + PARENTS.put(WEB_VIEW, ABSOLUTE_LAYOUT); + PARENTS.put(AUTO_COMPLETE_TEXT_VIEW, EDIT_TEXT); + PARENTS.put(MULTI_AUTO_COMPLETE_TEXT_VIEW, AUTO_COMPLETE_TEXT_VIEW); + PARENTS.put(CHECKED_TEXT_VIEW, TEXT_VIEW); + + PARENTS.put("MediaController", FRAME_LAYOUT); //$NON-NLS-1$ + PARENTS.put("SlidingDrawer", VIEW_GROUP); //$NON-NLS-1$ + PARENTS.put("DialerFilter", RELATIVE_LAYOUT); //$NON-NLS-1$ + PARENTS.put("DigitalClock", TEXT_VIEW); //$NON-NLS-1$ + PARENTS.put("Chronometer", TEXT_VIEW); //$NON-NLS-1$ + PARENTS.put("ImageSwitcher", VIEW_SWITCHER); //$NON-NLS-1$ + PARENTS.put("TextSwitcher", VIEW_SWITCHER); //$NON-NLS-1$ + PARENTS.put("AnalogClock", VIEW); //$NON-NLS-1$ + PARENTS.put("TwoLineListItem", RELATIVE_LAYOUT); //$NON-NLS-1$ + PARENTS.put("ZoomControls", LINEAR_LAYOUT); //$NON-NLS-1$ + PARENTS.put("DatePicker", FRAME_LAYOUT); //$NON-NLS-1$ + PARENTS.put("TimePicker", FRAME_LAYOUT); //$NON-NLS-1$ + PARENTS.put("VideoView", SURFACE_VIEW); //$NON-NLS-1$ + PARENTS.put("ZoomButton", IMAGE_BUTTON); //$NON-NLS-1$ + PARENTS.put("RatingBar", ABS_SEEK_BAR); //$NON-NLS-1$ + PARENTS.put("ViewFlipper", VIEW_ANIMATOR); //$NON-NLS-1$ + PARENTS.put("NumberPicker", LINEAR_LAYOUT); //$NON-NLS-1$ + + assert PARENTS.size() <= CLASS_COUNT : PARENTS.size(); + + /* + // Check that all widgets lead to the root view + if (LintUtils.assertionsEnabled()) { + for (String key : PARENTS.keySet()) { + String parent = PARENTS.get(key); + if (!parent.equals(VIEW)) { + String grandParent = PARENTS.get(parent); + assert grandParent != null : parent; + } + } + } + */ + + LAYOUTS.add(TAB_HOST); + LAYOUTS.add(HORIZONTAL_SCROLL_VIEW); + LAYOUTS.add(VIEW_SWITCHER); + LAYOUTS.add(TAB_WIDGET); + LAYOUTS.add(VIEW_ANIMATOR); + LAYOUTS.add(SCROLL_VIEW); + LAYOUTS.add(GRID_VIEW); + LAYOUTS.add(TABLE_ROW); + LAYOUTS.add(RADIO_GROUP); + LAYOUTS.add(LIST_VIEW); + LAYOUTS.add(EXPANDABLE_LIST_VIEW); + LAYOUTS.add("MediaController"); //$NON-NLS-1$ + LAYOUTS.add("DialerFilter"); //$NON-NLS-1$ + LAYOUTS.add("ViewFlipper"); //$NON-NLS-1$ + LAYOUTS.add("SlidingDrawer"); //$NON-NLS-1$ + LAYOUTS.add("StackView"); //$NON-NLS-1$ + LAYOUTS.add("SearchView"); //$NON-NLS-1$ + LAYOUTS.add("TextSwitcher"); //$NON-NLS-1$ + LAYOUTS.add("AdapterViewFlipper"); //$NON-NLS-1$ + LAYOUTS.add("ImageSwitcher"); //$NON-NLS-1$ + assert LAYOUTS.size() <= LAYOUT_COUNT : LAYOUTS.size(); + } + + // Currently using a map; this should really be a list, but using a map until we actually + // start adding more than one item + @NonNull + private static final Map INTERFACES = new HashMap(2); + static { + INTERFACES.put(CHECKED_TEXT_VIEW, CHECKABLE); + INTERFACES.put(COMPOUND_BUTTON, CHECKABLE); + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/IssueRegistry.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/IssueRegistry.java new file mode 100644 index 00000000000..09e70dc7e6b --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/IssueRegistry.java @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2011 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.client.api; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.tools.lint.detector.api.Category; +import com.android.tools.lint.detector.api.Detector; +import com.android.tools.lint.detector.api.Implementation; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.Scope; +import com.android.tools.lint.detector.api.Severity; +import com.google.common.annotations.Beta; +import com.google.common.collect.Maps; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Registry which provides a list of checks to be performed on an Android project + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public abstract class IssueRegistry { + private static List sCategories; + private static Map sIdToIssue; + private static Map, List> sScopeIssues = Maps.newHashMap(); + + /** + * Creates a new {@linkplain IssueRegistry} + */ + protected IssueRegistry() { + } + + private static final Implementation DUMMY_IMPLEMENTATION = new Implementation(Detector.class, + EnumSet.noneOf(Scope.class)); + /** + * Issue reported by lint (not a specific detector) when it cannot even + * parse an XML file prior to analysis + */ + @NonNull + public static final Issue PARSER_ERROR = Issue.create( + "ParserError", //$NON-NLS-1$ + "Parser Errors", + "Lint will ignore any files that contain fatal parsing errors. These may contain " + + "other errors, or contain code which affects issues in other files.", + Category.CORRECTNESS, + 10, + Severity.ERROR, + DUMMY_IMPLEMENTATION); + + /** + * Issue reported by lint for various other issues which prevents lint from + * running normally when it's not necessarily an error in the user's code base. + */ + @NonNull + public static final Issue LINT_ERROR = Issue.create( + "LintError", //$NON-NLS-1$ + "Lint Failure", + "This issue type represents a problem running lint itself. Examples include " + + "failure to find bytecode for source files (which means certain detectors " + + "could not be run), parsing errors in lint configuration files, etc." + + "\n" + + "These errors are not errors in your own code, but they are shown to make " + + "it clear that some checks were not completed.", + + Category.LINT, + 10, + Severity.ERROR, + DUMMY_IMPLEMENTATION); + + /** + * Issue reported when lint is canceled + */ + @NonNull + public static final Issue CANCELLED = Issue.create( + "LintCanceled", //$NON-NLS-1$ + "Lint Canceled", + "Lint canceled by user; the issue report may not be complete.", + + Category.LINT, + 0, + Severity.INFORMATIONAL, + DUMMY_IMPLEMENTATION); + + /** + * Returns the list of issues that can be found by all known detectors. + * + * @return the list of issues to be checked (including those that may be + * disabled!) + */ + @NonNull + public abstract List getIssues(); + + /** + * Get an approximate issue count for a given scope. This is just an optimization, + * so the number does not have to be accurate. + * + * @param scope the scope set + * @return an approximate ceiling of the number of issues expected for a given scope set + */ + protected int getIssueCapacity(@NonNull EnumSet scope) { + return 20; + } + + /** + * Returns all available issues of a given scope (regardless of whether + * they are actually enabled for a given configuration etc) + * + * @param scope the applicable scope set + * @return a list of issues + */ + @NonNull + protected List getIssuesForScope(@NonNull EnumSet scope) { + List list = sScopeIssues.get(scope); + if (list == null) { + List issues = getIssues(); + if (scope.equals(Scope.ALL)) { + list = issues; + } else { + list = new ArrayList(getIssueCapacity(scope)); + for (Issue issue : issues) { + // Determine if the scope matches + if (issue.getImplementation().isAdequate(scope)) { + list.add(issue); + } + } + } + sScopeIssues.put(scope, list); + } + + return list; + } + + /** + * Creates a list of detectors applicable to the given scope, and with the + * given configuration. + * + * @param client the client to report errors to + * @param configuration the configuration to look up which issues are + * enabled etc from + * @param scope the scope for the analysis, to filter out detectors that + * require wider analysis than is currently being performed + * @param scopeToDetectors an optional map which (if not null) will be + * filled by this method to contain mappings from each scope to + * the applicable detectors for that scope + * @return a list of new detector instances + */ + @NonNull + final List createDetectors( + @NonNull LintClient client, + @NonNull Configuration configuration, + @NonNull EnumSet scope, + @Nullable Map> scopeToDetectors) { + + List issues = getIssuesForScope(scope); + if (issues.isEmpty()) { + return Collections.emptyList(); + } + + Set> detectorClasses = new HashSet>(); + Map, EnumSet> detectorToScope = + new HashMap, EnumSet>(); + + for (Issue issue : issues) { + Implementation implementation = issue.getImplementation(); + Class detectorClass = implementation.getDetectorClass(); + EnumSet issueScope = implementation.getScope(); + if (!detectorClasses.contains(detectorClass)) { + // Determine if the issue is enabled + if (!configuration.isEnabled(issue)) { + continue; + } + + assert implementation.isAdequate(scope); // Ensured by getIssuesForScope above + + detectorClass = client.replaceDetector(detectorClass); + + assert detectorClass != null : issue.getId(); + detectorClasses.add(detectorClass); + } + + if (scopeToDetectors != null) { + EnumSet s = detectorToScope.get(detectorClass); + if (s == null) { + detectorToScope.put(detectorClass, issueScope); + } else if (!s.containsAll(issueScope)) { + EnumSet union = EnumSet.copyOf(s); + union.addAll(issueScope); + detectorToScope.put(detectorClass, union); + } + } + } + + List detectors = new ArrayList(detectorClasses.size()); + for (Class clz : detectorClasses) { + try { + Detector detector = clz.newInstance(); + detectors.add(detector); + + if (scopeToDetectors != null) { + EnumSet union = detectorToScope.get(clz); + for (Scope s : union) { + List list = scopeToDetectors.get(s); + if (list == null) { + list = new ArrayList(); + scopeToDetectors.put(s, list); + } + list.add(detector); + } + + } + } catch (Throwable t) { + client.log(t, "Can't initialize detector %1$s", clz.getName()); //$NON-NLS-1$ + } + } + + return detectors; + } + + /** + * Returns true if the given id represents a valid issue id + * + * @param id the id to be checked + * @return true if the given id is valid + */ + public final boolean isIssueId(@NonNull String id) { + return getIssue(id) != null; + } + + /** + * Returns true if the given category is a valid category + * + * @param name the category name to be checked + * @return true if the given string is a valid category + */ + public final boolean isCategoryName(@NonNull String name) { + for (Category category : getCategories()) { + if (category.getName().equals(name) || category.getFullName().equals(name)) { + return true; + } + } + + return false; + } + + /** + * Returns the available categories + * + * @return an iterator for all the categories, never null + */ + @NonNull + public List getCategories() { + if (sCategories == null) { + final Set categories = new HashSet(); + for (Issue issue : getIssues()) { + categories.add(issue.getCategory()); + } + List sorted = new ArrayList(categories); + Collections.sort(sorted); + sCategories = Collections.unmodifiableList(sorted); + } + + return sCategories; + } + + /** + * Returns the issue for the given id, or null if it's not a valid id + * + * @param id the id to be checked + * @return the corresponding issue, or null + */ + @Nullable + public final Issue getIssue(@NonNull String id) { + if (sIdToIssue == null) { + List issues = getIssues(); + sIdToIssue = new HashMap(issues.size()); + for (Issue issue : issues) { + sIdToIssue.put(issue.getId(), issue); + } + + sIdToIssue.put(PARSER_ERROR.getId(), PARSER_ERROR); + sIdToIssue.put(LINT_ERROR.getId(), LINT_ERROR); + } + return sIdToIssue.get(id); + } + + /** + * Reset the registry such that it recomputes its available issues. + *

+ * NOTE: This is only intended for testing purposes. + */ + @VisibleForTesting + protected static void reset() { + sIdToIssue = null; + sCategories = null; + sScopeIssues = Maps.newHashMap(); + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JarFileIssueRegistry.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JarFileIssueRegistry.java new file mode 100644 index 00000000000..0262691fc7e --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JarFileIssueRegistry.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2013 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.client.api; + +import com.android.annotations.NonNull; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.Severity; +import com.android.utils.SdkUtils; +import com.google.common.collect.Lists; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +/** + *

An {@link IssueRegistry} for a custom lint rule jar file. The rule jar should provide a + * manifest entry with the key {@code Lint-Registry} and the value of the fully qualified name of an + * implementation of {@link IssueRegistry} (with a default constructor).

+ * + *

NOTE: The custom issue registry should not extend this file; it should be a plain + * IssueRegistry! This file is used internally to wrap the given issue registry.

+ */ +class JarFileIssueRegistry extends IssueRegistry { + /** + * Manifest constant for declaring an issue provider. Example: Lint-Registry: + * foo.bar.CustomIssueRegistry + */ + private static final String MF_LINT_REGISTRY = "Lint-Registry"; //$NON-NLS-1$ + + private static Map> sCache; + + private final List myIssues; + + @NonNull + static IssueRegistry get(@NonNull LintClient client, @NonNull File jarFile) throws IOException, + ClassNotFoundException, IllegalAccessException, InstantiationException { + if (sCache == null) { + sCache = new HashMap>(); + } else { + SoftReference reference = sCache.get(jarFile); + if (reference != null) { + JarFileIssueRegistry registry = reference.get(); + if (registry != null) { + return registry; + } + } + } + + JarFileIssueRegistry registry = new JarFileIssueRegistry(client, jarFile); + sCache.put(jarFile, new SoftReference(registry)); + return registry; + } + + private JarFileIssueRegistry(@NonNull LintClient client, @NonNull File file) + throws IOException, ClassNotFoundException, IllegalAccessException, + InstantiationException { + myIssues = Lists.newArrayList(); + JarFile jarFile = null; + try { + jarFile = new JarFile(file); + Manifest manifest = jarFile.getManifest(); + Attributes attrs = manifest.getMainAttributes(); + Object object = attrs.get(new Attributes.Name(MF_LINT_REGISTRY)); + if (object instanceof String) { + String className = (String) object; + // Make a class loader for this jar + URL url = SdkUtils.fileToUrl(file); + URLClassLoader loader = new URLClassLoader(new URL[]{url}, + JarFileIssueRegistry.class.getClassLoader()); + Class registryClass = Class.forName(className, true, loader); + IssueRegistry registry = (IssueRegistry) registryClass.newInstance(); + myIssues.addAll(registry.getIssues()); + } else { + client.log(Severity.ERROR, null, + "Custom lint rule jar %1$s does not contain a valid registry manifest key " + + "(%2$s).\n" + + "Either the custom jar is invalid, or it uses an outdated API not supported " + + "this lint client", file.getPath(), MF_LINT_REGISTRY); + } + } finally { + if (jarFile != null) { + jarFile.close(); + } + } + } + + @NonNull + @Override + public List getIssues() { + return myIssues; + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaParser.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaParser.java new file mode 100644 index 00000000000..3aa55d42868 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaParser.java @@ -0,0 +1,530 @@ +/* + * Copyright (C) 2011 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.client.api; + +import static com.android.SdkConstants.ATTR_VALUE; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.lint.detector.api.Context; +import com.android.tools.lint.detector.api.JavaContext; +import com.android.tools.lint.detector.api.Location; +import com.google.common.annotations.Beta; +import com.google.common.base.Splitter; + +import java.util.Collections; +import java.util.List; + +import lombok.ast.Identifier; +import lombok.ast.Node; +import lombok.ast.StrictListAccessor; +import lombok.ast.TypeReference; +import lombok.ast.TypeReferencePart; + +/** + * A wrapper for a Java parser. This allows tools integrating lint to map directly + * to builtin services, such as already-parsed data structures in Java editors. + *

+ * NOTE: This is not public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public abstract class JavaParser { + public static final String TYPE_OBJECT = "java.lang.Object"; //$NON-NLS-1$ + public static final String TYPE_STRING = "java.lang.String"; //$NON-NLS-1$ + public static final String TYPE_INT = "int"; //$NON-NLS-1$ + public static final String TYPE_LONG = "long"; //$NON-NLS-1$ + public static final String TYPE_CHAR = "char"; //$NON-NLS-1$ + public static final String TYPE_FLOAT = "float"; //$NON-NLS-1$ + public static final String TYPE_DOUBLE = "double"; //$NON-NLS-1$ + public static final String TYPE_BOOLEAN = "boolean"; //$NON-NLS-1$ + public static final String TYPE_SHORT = "short"; //$NON-NLS-1$ + public static final String TYPE_BYTE = "byte"; //$NON-NLS-1$ + public static final String TYPE_NULL = "null"; //$NON-NLS-1$ + + /** + * Prepare to parse the given contexts. This method will be called before + * a series of {@link #parseJava(JavaContext)} calls, which allows some + * parsers to do up front global computation in case they want to more + * efficiently process multiple files at the same time. This allows a single + * type-attribution pass for example, which is a lot more efficient than + * performing global type analysis over and over again for each individual + * file + * + * @param contexts a list of contexts to be parsed + */ + public abstract void prepareJavaParse(@NonNull List contexts); + + /** + * Parse the file pointed to by the given context. + * + * @param context the context pointing to the file to be parsed, typically + * via {@link Context#getContents()} but the file handle ( + * {@link Context#file} can also be used to map to an existing + * editor buffer in the surrounding tool, etc) + * @return the compilation unit node for the file + */ + @Nullable + public abstract Node parseJava(@NonNull JavaContext context); + + /** + * Returns a {@link Location} for the given node + * + * @param context information about the file being parsed + * @param node the node to create a location for + * @return a location for the given node + */ + @NonNull + public abstract Location getLocation(@NonNull JavaContext context, @NonNull Node node); + + /** + * Creates a light-weight handle to a location for the given node. It can be + * turned into a full fledged location by + * {@link com.android.tools.lint.detector.api.Location.Handle#resolve()}. + * + * @param context the context providing the node + * @param node the node (element or attribute) to create a location handle + * for + * @return a location handle + */ + @NonNull + public abstract Location.Handle createLocationHandle(@NonNull JavaContext context, + @NonNull Node node); + + /** + * Dispose any data structures held for the given context. + * @param context information about the file previously parsed + * @param compilationUnit the compilation unit being disposed + */ + public void dispose(@NonNull JavaContext context, @NonNull Node compilationUnit) { + } + + /** + * Dispose any remaining data structures held for all contexts. + * Typically frees up any resources allocated by + * {@link #prepareJavaParse(List)} + */ + public void dispose() { + } + + /** + * Resolves the given expression node: computes the declaration for the given symbol + * + * @param context information about the file being parsed + * @param node the node to resolve + * @return a node representing the resolved fully type: class/interface/annotation, + * field, method or variable + */ + @Nullable + public abstract ResolvedNode resolve(@NonNull JavaContext context, @NonNull Node node); + + /** + * Finds the given type, if possible (which should be reachable from the compilation + * patch of the given node. + * + * @param context information about the file being parsed + * @param fullyQualifiedName the fully qualified name of the class to look up + * @return the class, or null if not found + */ + @Nullable + public ResolvedClass findClass( + @NonNull JavaContext context, + @NonNull String fullyQualifiedName) { + return null; + } + + /** + * Gets the type of the given node + * + * @param context information about the file being parsed + * @param node the node to look up the type for + * @return the type of the node, if known + */ + @Nullable + public abstract TypeDescriptor getType(@NonNull JavaContext context, @NonNull Node node); + + /** A description of a type, such as a primitive int or the android.app.Activity class */ + public abstract static class TypeDescriptor { + /** + * Returns the fully qualified name of the type, such as "int" or "android.app.Activity" + * */ + @NonNull public abstract String getName(); + + /** + * Returns the full signature of the type, which is normally the same as {@link #getName()} + * but for arrays can include []'s, for generic methods can include generics parameters + * etc + */ + @NonNull public abstract String getSignature(); + + public abstract boolean matchesName(@NonNull String name); + + public abstract boolean matchesSignature(@NonNull String signature); + + @NonNull + public TypeReference getNode() { + TypeReference typeReference = new TypeReference(); + StrictListAccessor parts = typeReference.astParts(); + for (String part : Splitter.on('.').split(getName())) { + Identifier identifier = Identifier.of(part); + parts.addToEnd(new TypeReferencePart().astIdentifier(identifier)); + } + + return typeReference; + } + + /** If the type is not primitive, returns the class of the type if known */ + @Nullable + public abstract ResolvedClass getTypeClass(); + + @Override + public abstract boolean equals(Object o); + + } + + /** Convenience implementation of {@link TypeDescriptor} */ + public static class DefaultTypeDescriptor extends TypeDescriptor { + + private String mName; + + public DefaultTypeDescriptor(String name) { + mName = name; + } + + @NonNull + @Override + public String getName() { + return mName; + } + + @NonNull + @Override + public String getSignature() { + return getName(); + } + + @Override + public boolean matchesName(@NonNull String name) { + return mName.equals(name); + } + + @Override + public boolean matchesSignature(@NonNull String signature) { + return matchesName(signature); + } + + @Override + public String toString() { + return getSignature(); + } + + @Override + @Nullable + public ResolvedClass getTypeClass() { + return null; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DefaultTypeDescriptor that = (DefaultTypeDescriptor) o; + + return !(mName != null ? !mName.equals(that.mName) : that.mName != null); + + } + + @Override + public int hashCode() { + return mName != null ? mName.hashCode() : 0; + } + } + + /** A resolved declaration from an AST Node reference */ + public abstract static class ResolvedNode { + @NonNull + public abstract String getName(); + + /** Returns the signature of the resolved node */ + public abstract String getSignature(); + + public abstract int getModifiers(); + + @Override + public String toString() { + return getSignature(); + } + + /** Returns any annotations defined on this node */ + @NonNull + public abstract Iterable getAnnotations(); + + /** + * Searches for the annotation of the given type on this node + * + * @param type the fully qualified name of the annotation to check + * @return the annotation, or null if not found + */ + @Nullable + public ResolvedAnnotation getAnnotation(@NonNull String type) { + for (ResolvedAnnotation annotation : getAnnotations()) { + if (annotation.getType().matchesSignature(type)) { + return annotation; + } + } + + return null; + } + } + + /** A resolved class declaration (class, interface, enumeration or annotation) */ + public abstract static class ResolvedClass extends ResolvedNode { + /** Returns the fully qualified name of this class */ + @Override + @NonNull + public abstract String getName(); + + /** Returns the simple of this class */ + @NonNull + public abstract String getSimpleName(); + + /** Returns the package name of this class */ + @NonNull + public String getPackageName() { + String name = getName(); + String simpleName = getSimpleName(); + if (name.length() > simpleName.length() + 1) { + return name.substring(0, name.length() - simpleName.length() - 1); + } + return name; + } + + /** Returns whether this class' fully qualified name matches the given name */ + public abstract boolean matches(@NonNull String name); + + @Nullable + public abstract ResolvedClass getSuperClass(); + + @Nullable + public abstract ResolvedClass getContainingClass(); + + public TypeDescriptor getType() { + return new DefaultTypeDescriptor(getName()); + } + + /** + * Determines whether this class extends the given name. If strict is true, + * it will not consider C extends C true. + * + * @param name the fully qualified class name + * @param strict if true, do not consider a class to be extending itself + * @return true if this class extends the given class + */ + public abstract boolean isSubclassOf(@NonNull String name, boolean strict); + + @NonNull + public abstract Iterable getConstructors(); + + /** Returns the methods defined in this class, and optionally any methods inherited from any superclasses as well */ + @NonNull + public abstract Iterable getMethods(boolean includeInherited); + + /** Returns the methods of a given name defined in this class, and optionally any methods inherited from any superclasses as well */ + @NonNull + public abstract Iterable getMethods(@NonNull String name, boolean includeInherited); + + /** Returns the named field defined in this class, or optionally inherited from a superclass */ + @Nullable + public abstract ResolvedField getField(@NonNull String name, boolean includeInherited); + } + + /** A method or constructor declaration */ + public abstract static class ResolvedMethod extends ResolvedNode { + @Override + @NonNull + public abstract String getName(); + + /** Returns whether this method name matches the given name */ + public abstract boolean matches(@NonNull String name); + + @NonNull + public abstract ResolvedClass getContainingClass(); + + public abstract int getArgumentCount(); + + @NonNull + public abstract TypeDescriptor getArgumentType(int index); + + /** Returns true if the parameter at the given index matches the given type signature */ + public boolean argumentMatchesType(int index, @NonNull String signature) { + return getArgumentType(index).matchesSignature(signature); + } + + @Nullable + public abstract TypeDescriptor getReturnType(); + + public boolean isConstructor() { + return getReturnType() == null; + } + + /** Returns any annotations defined on the given parameter of this method */ + @NonNull + public abstract Iterable getParameterAnnotations(int index); + + /** + * Searches for the annotation of the given type on the method + * + * @param type the fully qualified name of the annotation to check + * @param parameterIndex the index of the parameter to look up + * @return the annotation, or null if not found + */ + @Nullable + public ResolvedAnnotation getParameterAnnotation(@NonNull String type, + int parameterIndex) { + for (ResolvedAnnotation annotation : getParameterAnnotations(parameterIndex)) { + if (annotation.getType().matchesSignature(type)) { + return annotation; + } + } + + return null; + } + + /** Returns the super implementation of the given method, if any */ + @Nullable + public ResolvedMethod getSuperMethod() { + ResolvedClass cls = getContainingClass().getSuperClass(); + if (cls != null) { + String methodName = getName(); + int argCount = getArgumentCount(); + for (ResolvedMethod method : cls.getMethods(methodName, true)) { + if (argCount != method.getArgumentCount()) { + continue; + } + boolean sameTypes = true; + for (int arg = 0; arg < argCount; arg++) { + if (!method.getArgumentType(arg).equals(getArgumentType(arg))) { + sameTypes = false; + break; + } + } + if (sameTypes) { + return method; + } + } + } + + return null; + } + } + + /** A field declaration */ + public abstract static class ResolvedField extends ResolvedNode { + @Override + @NonNull + public abstract String getName(); + + /** Returns whether this field name matches the given name */ + public abstract boolean matches(@NonNull String name); + + @NonNull + public abstract TypeDescriptor getType(); + + @NonNull + public abstract ResolvedClass getContainingClass(); + + @Nullable + public abstract Object getValue(); + + public String getContainingClassName() { + return getContainingClass().getName(); + } + } + + /** + * An annotation reference. Note that this refers to a usage of an annotation, + * not a declaraton of an annotation. You can call {@link #getClassType()} to + * find the declaration for the annotation. + */ + public abstract static class ResolvedAnnotation extends ResolvedNode { + @Override + @NonNull + public abstract String getName(); + + /** Returns whether this field name matches the given name */ + public abstract boolean matches(@NonNull String name); + + @NonNull + public abstract TypeDescriptor getType(); + + /** Returns the {@link ResolvedClass} which defines the annotation */ + @Nullable + public abstract ResolvedClass getClassType(); + + public static class Value { + @NonNull public final String name; + @Nullable public final Object value; + + public Value(@NonNull String name, @Nullable Object value) { + this.name = name; + this.value = value; + } + } + + @NonNull + public abstract List getValues(); + + @Nullable + public Object getValue(@NonNull String name) { + for (Value value : getValues()) { + if (name.equals(value.name)) { + return value.value; + } + } + return null; + } + + @Nullable + public Object getValue() { + return getValue(ATTR_VALUE); + } + + @NonNull + @Override + public Iterable getAnnotations() { + return Collections.emptyList(); + } + } + + /** A local variable or parameter declaration */ + public abstract static class ResolvedVariable extends ResolvedNode { + @Override + @NonNull + public abstract String getName(); + + /** Returns whether this variable name matches the given name */ + public abstract boolean matches(@NonNull String name); + + @NonNull + public abstract TypeDescriptor getType(); + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaVisitor.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaVisitor.java new file mode 100644 index 00000000000..e852925bc19 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/JavaVisitor.java @@ -0,0 +1,1398 @@ +/* + * Copyright (C) 2011 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.client.api; + +import static com.android.SdkConstants.ANDROID_PKG; +import static com.android.SdkConstants.R_CLASS; + +import com.android.annotations.NonNull; +import com.android.tools.lint.client.api.JavaParser.ResolvedClass; +import com.android.tools.lint.client.api.JavaParser.ResolvedMethod; +import com.android.tools.lint.client.api.JavaParser.ResolvedNode; +import com.android.tools.lint.detector.api.Detector; +import com.android.tools.lint.detector.api.Detector.JavaScanner; +import com.android.tools.lint.detector.api.Detector.XmlScanner; +import com.android.tools.lint.detector.api.JavaContext; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import lombok.ast.AlternateConstructorInvocation; +import lombok.ast.Annotation; +import lombok.ast.AnnotationDeclaration; +import lombok.ast.AnnotationElement; +import lombok.ast.AnnotationMethodDeclaration; +import lombok.ast.AnnotationValueArray; +import lombok.ast.ArrayAccess; +import lombok.ast.ArrayCreation; +import lombok.ast.ArrayDimension; +import lombok.ast.ArrayInitializer; +import lombok.ast.Assert; +import lombok.ast.AstVisitor; +import lombok.ast.BinaryExpression; +import lombok.ast.Block; +import lombok.ast.BooleanLiteral; +import lombok.ast.Break; +import lombok.ast.Case; +import lombok.ast.Cast; +import lombok.ast.Catch; +import lombok.ast.CharLiteral; +import lombok.ast.ClassDeclaration; +import lombok.ast.ClassLiteral; +import lombok.ast.Comment; +import lombok.ast.CompilationUnit; +import lombok.ast.ConstructorDeclaration; +import lombok.ast.ConstructorInvocation; +import lombok.ast.Continue; +import lombok.ast.Default; +import lombok.ast.DoWhile; +import lombok.ast.EmptyDeclaration; +import lombok.ast.EmptyStatement; +import lombok.ast.EnumConstant; +import lombok.ast.EnumDeclaration; +import lombok.ast.EnumTypeBody; +import lombok.ast.Expression; +import lombok.ast.ExpressionStatement; +import lombok.ast.FloatingPointLiteral; +import lombok.ast.For; +import lombok.ast.ForEach; +import lombok.ast.ForwardingAstVisitor; +import lombok.ast.Identifier; +import lombok.ast.If; +import lombok.ast.ImportDeclaration; +import lombok.ast.InlineIfExpression; +import lombok.ast.InstanceInitializer; +import lombok.ast.InstanceOf; +import lombok.ast.IntegralLiteral; +import lombok.ast.InterfaceDeclaration; +import lombok.ast.KeywordModifier; +import lombok.ast.LabelledStatement; +import lombok.ast.MethodDeclaration; +import lombok.ast.MethodInvocation; +import lombok.ast.Modifiers; +import lombok.ast.Node; +import lombok.ast.NormalTypeBody; +import lombok.ast.NullLiteral; +import lombok.ast.PackageDeclaration; +import lombok.ast.Return; +import lombok.ast.Select; +import lombok.ast.StaticInitializer; +import lombok.ast.StringLiteral; +import lombok.ast.Super; +import lombok.ast.SuperConstructorInvocation; +import lombok.ast.Switch; +import lombok.ast.Synchronized; +import lombok.ast.This; +import lombok.ast.Throw; +import lombok.ast.Try; +import lombok.ast.TypeReference; +import lombok.ast.TypeReferencePart; +import lombok.ast.TypeVariable; +import lombok.ast.UnaryExpression; +import lombok.ast.VariableDeclaration; +import lombok.ast.VariableDefinition; +import lombok.ast.VariableDefinitionEntry; +import lombok.ast.VariableReference; +import lombok.ast.While; + +/** + * Specialized visitor for running detectors on a Java AST. + * It operates in three phases: + *

    + *
  1. First, it computes a set of maps where it generates a map from each + * significant AST attribute (such as method call names) to a list + * of detectors to consult whenever that attribute is encountered. + * Examples of "attributes" are method names, Android resource identifiers, + * and general AST node types such as "cast" nodes etc. These are + * defined on the {@link JavaScanner} interface. + *
  2. Second, it iterates over the document a single time, delegating to + * the detectors found at each relevant AST attribute. + *
  3. Finally, it calls the remaining visitors (those that need to process a + * whole document on their own). + *
+ * It also notifies all the detectors before and after the document is processed + * such that they can do pre- and post-processing. + */ +public class JavaVisitor { + /** Default size of lists holding detectors of the same type for a given node type */ + private static final int SAME_TYPE_COUNT = 8; + + private final Map> mMethodDetectors = + Maps.newHashMapWithExpectedSize(40); + private final Map> mConstructorDetectors = + Maps.newHashMapWithExpectedSize(12); + private Set mConstructorSimpleNames; + private final List mResourceFieldDetectors = + new ArrayList(); + private final List mAllDetectors; + private final List mFullTreeDetectors; + private final Map, List> mNodeTypeDetectors = + new HashMap, List>(16); + private final JavaParser mParser; + private final Map> mSuperClassDetectors = + new HashMap>(); + + /** + * Number of fatal exceptions (internal errors, usually from ECJ) we've + * encountered; we don't log each and every one to avoid massive log spam + * in code which triggers this condition + */ + private static int sExceptionCount; + /** Max number of logs to include */ + private static final int MAX_REPORTED_CRASHES = 20; + + JavaVisitor(@NonNull JavaParser parser, @NonNull List detectors) { + mParser = parser; + mAllDetectors = new ArrayList(detectors.size()); + mFullTreeDetectors = new ArrayList(detectors.size()); + + for (Detector detector : detectors) { + VisitingDetector v = new VisitingDetector(detector, (JavaScanner) detector); + mAllDetectors.add(v); + + List applicableSuperClasses = detector.applicableSuperClasses(); + if (applicableSuperClasses != null) { + for (String fqn : applicableSuperClasses) { + List list = mSuperClassDetectors.get(fqn); + if (list == null) { + list = new ArrayList(SAME_TYPE_COUNT); + mSuperClassDetectors.put(fqn, list); + } + list.add(v); + } + continue; + } + + List> nodeTypes = detector.getApplicableNodeTypes(); + if (nodeTypes != null) { + for (Class type : nodeTypes) { + List list = mNodeTypeDetectors.get(type); + if (list == null) { + list = new ArrayList(SAME_TYPE_COUNT); + mNodeTypeDetectors.put(type, list); + } + list.add(v); + } + } + + List names = detector.getApplicableMethodNames(); + if (names != null) { + // not supported in Java visitors; adding a method invocation node is trivial + // for that case. + assert names != XmlScanner.ALL; + + for (String name : names) { + List list = mMethodDetectors.get(name); + if (list == null) { + list = new ArrayList(SAME_TYPE_COUNT); + mMethodDetectors.put(name, list); + } + list.add(v); + } + } + + List types = detector.getApplicableConstructorTypes(); + if (types != null) { + // not supported in Java visitors; adding a method invocation node is trivial + // for that case. + assert types != XmlScanner.ALL; + if (mConstructorSimpleNames == null) { + mConstructorSimpleNames = Sets.newHashSet(); + } + for (String type : types) { + List list = mConstructorDetectors.get(type); + if (list == null) { + list = new ArrayList(SAME_TYPE_COUNT); + mConstructorDetectors.put(type, list); + mConstructorSimpleNames.add(type.substring(type.lastIndexOf('.')+1)); + } + list.add(v); + } + } + + if (detector.appliesToResourceRefs()) { + mResourceFieldDetectors.add(v); + } else if ((names == null || names.isEmpty()) + && (nodeTypes == null || nodeTypes.isEmpty()) + && (types == null || types.isEmpty())) { + mFullTreeDetectors.add(v); + } + } + } + + void visitFile(@NonNull JavaContext context) { + Node compilationUnit = null; + try { + compilationUnit = mParser.parseJava(context); + if (compilationUnit == null) { + // No need to log this; the parser should be reporting + // a full warning (such as IssueRegistry#PARSER_ERROR) + // with details, location, etc. + return; + } + context.setCompilationUnit(compilationUnit); + + for (VisitingDetector v : mAllDetectors) { + v.setContext(context); + v.getDetector().beforeCheckFile(context); + } + + if (!mSuperClassDetectors.isEmpty()) { + SuperclassVisitor visitor = new SuperclassVisitor(context); + compilationUnit.accept(visitor); + } + + for (VisitingDetector v : mFullTreeDetectors) { + AstVisitor visitor = v.getVisitor(); + compilationUnit.accept(visitor); + } + + if (!mMethodDetectors.isEmpty() || !mResourceFieldDetectors.isEmpty() || + !mConstructorDetectors.isEmpty()) { + AstVisitor visitor = new DelegatingJavaVisitor(context); + compilationUnit.accept(visitor); + } else if (!mNodeTypeDetectors.isEmpty()) { + AstVisitor visitor = new DispatchVisitor(); + compilationUnit.accept(visitor); + } + + for (VisitingDetector v : mAllDetectors) { + v.getDetector().afterCheckFile(context); + } + } catch (RuntimeException e) { + if (sExceptionCount++ > MAX_REPORTED_CRASHES) { + // No need to keep spamming the user that a lot of the files + // are tripping up ECJ, they get the picture. + return; + } + + // Work around ECJ bugs; see https://code.google.com/p/android/issues/detail?id=172268 + // Don't allow lint bugs to take down the whole build. TRY to log this as a + // lint error instead! + StringBuilder sb = new StringBuilder(100); + sb.append("Unexpected failure during lint analysis of "); + sb.append(context.file.getName()); + sb.append(" (this is a bug in lint or one of the libraries it depends on)\n"); + + StackTraceElement[] stackTrace = e.getStackTrace(); + int count = 0; + for (StackTraceElement frame : stackTrace) { + if (count > 0) { + sb.append("->"); + } + + String className = frame.getClassName(); + sb.append(className.substring(className.lastIndexOf('.') + 1)); + sb.append('.').append(frame.getMethodName()); + sb.append('('); + sb.append(frame.getFileName()).append(':').append(frame.getLineNumber()); + sb.append(')'); + count++; + // Only print the top 3-4 frames such that we can identify the bug + if (count == 4) { + break; + } + } + Throwable throwable = null; // NOT e: this makes for very noisy logs + //noinspection ConstantConditions + context.log(throwable, sb.toString()); + } finally { + if (compilationUnit != null) { + mParser.dispose(context, compilationUnit); + } + } + } + + public void prepare(@NonNull List contexts) { + mParser.prepareJavaParse(contexts); + } + + public void dispose() { + mParser.dispose(); + } + + private static class VisitingDetector { + private AstVisitor mVisitor; // construct lazily, and clear out on context switch! + private JavaContext mContext; + public final Detector mDetector; + public final JavaScanner mJavaScanner; + + public VisitingDetector(@NonNull Detector detector, @NonNull JavaScanner javaScanner) { + mDetector = detector; + mJavaScanner = javaScanner; + } + + @NonNull + public Detector getDetector() { + return mDetector; + } + + @NonNull + public JavaScanner getJavaScanner() { + return mJavaScanner; + } + + public void setContext(@NonNull JavaContext context) { + mContext = context; + + // The visitors are one-per-context, so clear them out here and construct + // lazily only if needed + mVisitor = null; + } + + @NonNull + AstVisitor getVisitor() { + if (mVisitor == null) { + mVisitor = mDetector.createJavaVisitor(mContext); + if (mVisitor == null) { + mVisitor = new ForwardingAstVisitor() { + }; + } + } + return mVisitor; + } + } + + private class SuperclassVisitor extends ForwardingAstVisitor { + private JavaContext mContext; + + public SuperclassVisitor(@NonNull JavaContext context) { + mContext = context; + } + + @Override + public boolean visitClassDeclaration(ClassDeclaration node) { + ResolvedNode resolved = mContext.resolve(node); + if (!(resolved instanceof ResolvedClass)) { + return true; + } + + ResolvedClass resolvedClass = (ResolvedClass) resolved; + ResolvedClass cls = resolvedClass; + while (cls != null) { + String fqcn = cls.getSignature(); + if (fqcn != null) { + List list = mSuperClassDetectors.get(fqcn); + if (list != null) { + for (VisitingDetector v : list) { + v.getJavaScanner().checkClass(mContext, node, node, resolvedClass); + } + } + } + + cls = cls.getSuperClass(); + } + + return false; + } + + @Override + public boolean visitConstructorInvocation(ConstructorInvocation node) { + NormalTypeBody anonymous = node.astAnonymousClassBody(); + if (anonymous != null) { + ResolvedNode resolved = mContext.resolve(anonymous); + if (resolved instanceof ResolvedMethod) { + resolved = ((ResolvedMethod) resolved).getContainingClass(); + } + if (!(resolved instanceof ResolvedClass)) { + return true; + } + + ResolvedClass resolvedClass = (ResolvedClass) resolved; + ResolvedClass cls = resolvedClass; + while (cls != null) { + String fqcn = cls.getSignature(); + if (fqcn != null) { + List list = mSuperClassDetectors.get(fqcn); + if (list != null) { + for (VisitingDetector v : list) { + v.getJavaScanner().checkClass(mContext, null, anonymous, + resolvedClass); + } + } + } + + cls = cls.getSuperClass(); + } + } + + return true; + } + + @Override + public boolean visitImportDeclaration(ImportDeclaration node) { + return true; + } + } + + /** + * Generic dispatcher which visits all nodes (once) and dispatches to + * specific visitors for each node. Each visitor typically only wants to + * look at a small part of a tree, such as a method call or a class + * declaration, so this means we avoid visiting all "uninteresting" nodes in + * the tree repeatedly. + */ + private class DispatchVisitor extends ForwardingAstVisitor { + @Override + public void endVisit(Node node) { + for (VisitingDetector v : mAllDetectors) { + v.getVisitor().endVisit(node); + } + } + + @Override + public boolean visitAlternateConstructorInvocation(AlternateConstructorInvocation node) { + List list = + mNodeTypeDetectors.get(AlternateConstructorInvocation.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitAlternateConstructorInvocation(node); + } + } + return false; + } + + @Override + public boolean visitAnnotation(Annotation node) { + List list = mNodeTypeDetectors.get(Annotation.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitAnnotation(node); + } + } + return false; + } + + @Override + public boolean visitAnnotationDeclaration(AnnotationDeclaration node) { + List list = mNodeTypeDetectors.get(AnnotationDeclaration.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitAnnotationDeclaration(node); + } + } + return false; + } + + @Override + public boolean visitAnnotationElement(AnnotationElement node) { + List list = mNodeTypeDetectors.get(AnnotationElement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitAnnotationElement(node); + } + } + return false; + } + + @Override + public boolean visitAnnotationMethodDeclaration(AnnotationMethodDeclaration node) { + List list = + mNodeTypeDetectors.get(AnnotationMethodDeclaration.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitAnnotationMethodDeclaration(node); + } + } + return false; + } + + @Override + public boolean visitAnnotationValueArray(AnnotationValueArray node) { + List list = mNodeTypeDetectors.get(AnnotationValueArray.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitAnnotationValueArray(node); + } + } + return false; + } + + @Override + public boolean visitArrayAccess(ArrayAccess node) { + List list = mNodeTypeDetectors.get(ArrayAccess.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitArrayAccess(node); + } + } + return false; + } + + @Override + public boolean visitArrayCreation(ArrayCreation node) { + List list = mNodeTypeDetectors.get(ArrayCreation.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitArrayCreation(node); + } + } + return false; + } + + @Override + public boolean visitArrayDimension(ArrayDimension node) { + List list = mNodeTypeDetectors.get(ArrayDimension.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitArrayDimension(node); + } + } + return false; + } + + @Override + public boolean visitArrayInitializer(ArrayInitializer node) { + List list = mNodeTypeDetectors.get(ArrayInitializer.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitArrayInitializer(node); + } + } + return false; + } + + @Override + public boolean visitAssert(Assert node) { + List list = mNodeTypeDetectors.get(Assert.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitAssert(node); + } + } + return false; + } + + @Override + public boolean visitBinaryExpression(BinaryExpression node) { + List list = mNodeTypeDetectors.get(BinaryExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitBinaryExpression(node); + } + } + return false; + } + + @Override + public boolean visitBlock(Block node) { + List list = mNodeTypeDetectors.get(Block.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitBlock(node); + } + } + return false; + } + + @Override + public boolean visitBooleanLiteral(BooleanLiteral node) { + List list = mNodeTypeDetectors.get(BooleanLiteral.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitBooleanLiteral(node); + } + } + return false; + } + + @Override + public boolean visitBreak(Break node) { + List list = mNodeTypeDetectors.get(Break.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitBreak(node); + } + } + return false; + } + + @Override + public boolean visitCase(Case node) { + List list = mNodeTypeDetectors.get(Case.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitCase(node); + } + } + return false; + } + + @Override + public boolean visitCast(Cast node) { + List list = mNodeTypeDetectors.get(Cast.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitCast(node); + } + } + return false; + } + + @Override + public boolean visitCatch(Catch node) { + List list = mNodeTypeDetectors.get(Catch.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitCatch(node); + } + } + return false; + } + + @Override + public boolean visitCharLiteral(CharLiteral node) { + List list = mNodeTypeDetectors.get(CharLiteral.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitCharLiteral(node); + } + } + return false; + } + + @Override + public boolean visitClassDeclaration(ClassDeclaration node) { + List list = mNodeTypeDetectors.get(ClassDeclaration.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitClassDeclaration(node); + } + } + return false; + } + + @Override + public boolean visitClassLiteral(ClassLiteral node) { + List list = mNodeTypeDetectors.get(ClassLiteral.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitClassLiteral(node); + } + } + return false; + } + + @Override + public boolean visitComment(Comment node) { + List list = mNodeTypeDetectors.get(Comment.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitComment(node); + } + } + return false; + } + + @Override + public boolean visitCompilationUnit(CompilationUnit node) { + List list = mNodeTypeDetectors.get(CompilationUnit.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitCompilationUnit(node); + } + } + return false; + } + + @Override + public boolean visitConstructorDeclaration(ConstructorDeclaration node) { + List list = mNodeTypeDetectors.get(ConstructorDeclaration.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitConstructorDeclaration(node); + } + } + return false; + } + + @Override + public boolean visitConstructorInvocation(ConstructorInvocation node) { + List list = mNodeTypeDetectors.get(ConstructorInvocation.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitConstructorInvocation(node); + } + } + return false; + } + + @Override + public boolean visitContinue(Continue node) { + List list = mNodeTypeDetectors.get(Continue.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitContinue(node); + } + } + return false; + } + + @Override + public boolean visitDefault(Default node) { + List list = mNodeTypeDetectors.get(Default.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitDefault(node); + } + } + return false; + } + + @Override + public boolean visitDoWhile(DoWhile node) { + List list = mNodeTypeDetectors.get(DoWhile.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitDoWhile(node); + } + } + return false; + } + + @Override + public boolean visitEmptyDeclaration(EmptyDeclaration node) { + List list = mNodeTypeDetectors.get(EmptyDeclaration.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitEmptyDeclaration(node); + } + } + return false; + } + + @Override + public boolean visitEmptyStatement(EmptyStatement node) { + List list = mNodeTypeDetectors.get(EmptyStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitEmptyStatement(node); + } + } + return false; + } + + @Override + public boolean visitEnumConstant(EnumConstant node) { + List list = mNodeTypeDetectors.get(EnumConstant.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitEnumConstant(node); + } + } + return false; + } + + @Override + public boolean visitEnumDeclaration(EnumDeclaration node) { + List list = mNodeTypeDetectors.get(EnumDeclaration.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitEnumDeclaration(node); + } + } + return false; + } + + @Override + public boolean visitEnumTypeBody(EnumTypeBody node) { + List list = mNodeTypeDetectors.get(EnumTypeBody.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitEnumTypeBody(node); + } + } + return false; + } + + @Override + public boolean visitExpressionStatement(ExpressionStatement node) { + List list = mNodeTypeDetectors.get(ExpressionStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitExpressionStatement(node); + } + } + return false; + } + + @Override + public boolean visitFloatingPointLiteral(FloatingPointLiteral node) { + List list = mNodeTypeDetectors.get(FloatingPointLiteral.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitFloatingPointLiteral(node); + } + } + return false; + } + + @Override + public boolean visitFor(For node) { + List list = mNodeTypeDetectors.get(For.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitFor(node); + } + } + return false; + } + + @Override + public boolean visitForEach(ForEach node) { + List list = mNodeTypeDetectors.get(ForEach.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitForEach(node); + } + } + return false; + } + + @Override + public boolean visitIdentifier(Identifier node) { + List list = mNodeTypeDetectors.get(Identifier.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitIdentifier(node); + } + } + return false; + } + + @Override + public boolean visitIf(If node) { + List list = mNodeTypeDetectors.get(If.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitIf(node); + } + } + return false; + } + + @Override + public boolean visitImportDeclaration(ImportDeclaration node) { + List list = mNodeTypeDetectors.get(ImportDeclaration.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitImportDeclaration(node); + } + } + return false; + } + + @Override + public boolean visitInlineIfExpression(InlineIfExpression node) { + List list = mNodeTypeDetectors.get(InlineIfExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitInlineIfExpression(node); + } + } + return false; + } + + @Override + public boolean visitInstanceInitializer(InstanceInitializer node) { + List list = mNodeTypeDetectors.get(InstanceInitializer.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitInstanceInitializer(node); + } + } + return false; + } + + @Override + public boolean visitInstanceOf(InstanceOf node) { + List list = mNodeTypeDetectors.get(InstanceOf.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitInstanceOf(node); + } + } + return false; + } + + @Override + public boolean visitIntegralLiteral(IntegralLiteral node) { + List list = mNodeTypeDetectors.get(IntegralLiteral.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitIntegralLiteral(node); + } + } + return false; + } + + @Override + public boolean visitInterfaceDeclaration(InterfaceDeclaration node) { + List list = mNodeTypeDetectors.get(InterfaceDeclaration.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitInterfaceDeclaration(node); + } + } + return false; + } + + @Override + public boolean visitKeywordModifier(KeywordModifier node) { + List list = mNodeTypeDetectors.get(KeywordModifier.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitKeywordModifier(node); + } + } + return false; + } + + @Override + public boolean visitLabelledStatement(LabelledStatement node) { + List list = mNodeTypeDetectors.get(LabelledStatement.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitLabelledStatement(node); + } + } + return false; + } + + @Override + public boolean visitMethodDeclaration(MethodDeclaration node) { + List list = mNodeTypeDetectors.get(MethodDeclaration.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitMethodDeclaration(node); + } + } + return false; + } + + @Override + public boolean visitMethodInvocation(MethodInvocation node) { + List list = mNodeTypeDetectors.get(MethodInvocation.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitMethodInvocation(node); + } + } + return false; + } + + @Override + public boolean visitModifiers(Modifiers node) { + List list = mNodeTypeDetectors.get(Modifiers.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitModifiers(node); + } + } + return false; + } + + @Override + public boolean visitNormalTypeBody(NormalTypeBody node) { + List list = mNodeTypeDetectors.get(NormalTypeBody.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitNormalTypeBody(node); + } + } + return false; + } + + @Override + public boolean visitNullLiteral(NullLiteral node) { + List list = mNodeTypeDetectors.get(NullLiteral.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitNullLiteral(node); + } + } + return false; + } + + @Override + public boolean visitPackageDeclaration(PackageDeclaration node) { + List list = mNodeTypeDetectors.get(PackageDeclaration.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitPackageDeclaration(node); + } + } + return false; + } + + @Override + public boolean visitParseArtefact(Node node) { + List list = mNodeTypeDetectors.get(Node.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitParseArtefact(node); + } + } + return false; + } + + @Override + public boolean visitReturn(Return node) { + List list = mNodeTypeDetectors.get(Return.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitReturn(node); + } + } + return false; + } + + @Override + public boolean visitSelect(Select node) { + List list = mNodeTypeDetectors.get(Select.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitSelect(node); + } + } + return false; + } + + @Override + public boolean visitStaticInitializer(StaticInitializer node) { + List list = mNodeTypeDetectors.get(StaticInitializer.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitStaticInitializer(node); + } + } + return false; + } + + @Override + public boolean visitStringLiteral(StringLiteral node) { + List list = mNodeTypeDetectors.get(StringLiteral.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitStringLiteral(node); + } + } + return false; + } + + @Override + public boolean visitSuper(Super node) { + List list = mNodeTypeDetectors.get(Super.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitSuper(node); + } + } + return false; + } + + @Override + public boolean visitSuperConstructorInvocation(SuperConstructorInvocation node) { + List list = mNodeTypeDetectors.get(SuperConstructorInvocation.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitSuperConstructorInvocation(node); + } + } + return false; + } + + @Override + public boolean visitSwitch(Switch node) { + List list = mNodeTypeDetectors.get(Switch.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitSwitch(node); + } + } + return false; + } + + @Override + public boolean visitSynchronized(Synchronized node) { + List list = mNodeTypeDetectors.get(Synchronized.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitSynchronized(node); + } + } + return false; + } + + @Override + public boolean visitThis(This node) { + List list = mNodeTypeDetectors.get(This.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitThis(node); + } + } + return false; + } + + @Override + public boolean visitThrow(Throw node) { + List list = mNodeTypeDetectors.get(Throw.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitThrow(node); + } + } + return false; + } + + @Override + public boolean visitTry(Try node) { + List list = mNodeTypeDetectors.get(Try.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitTry(node); + } + } + return false; + } + + @Override + public boolean visitTypeReference(TypeReference node) { + List list = mNodeTypeDetectors.get(TypeReference.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitTypeReference(node); + } + } + return false; + } + + @Override + public boolean visitTypeReferencePart(TypeReferencePart node) { + List list = mNodeTypeDetectors.get(TypeReferencePart.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitTypeReferencePart(node); + } + } + return false; + } + + @Override + public boolean visitTypeVariable(TypeVariable node) { + List list = mNodeTypeDetectors.get(TypeVariable.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitTypeVariable(node); + } + } + return false; + } + + @Override + public boolean visitUnaryExpression(UnaryExpression node) { + List list = mNodeTypeDetectors.get(UnaryExpression.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitUnaryExpression(node); + } + } + return false; + } + + @Override + public boolean visitVariableDeclaration(VariableDeclaration node) { + List list = mNodeTypeDetectors.get(VariableDeclaration.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitVariableDeclaration(node); + } + } + return false; + } + + @Override + public boolean visitVariableDefinition(VariableDefinition node) { + List list = mNodeTypeDetectors.get(VariableDefinition.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitVariableDefinition(node); + } + } + return false; + } + + @Override + public boolean visitVariableDefinitionEntry(VariableDefinitionEntry node) { + List list = mNodeTypeDetectors.get(VariableDefinitionEntry.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitVariableDefinitionEntry(node); + } + } + return false; + } + + @Override + public boolean visitVariableReference(VariableReference node) { + List list = mNodeTypeDetectors.get(VariableReference.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitVariableReference(node); + } + } + return false; + } + + @Override + public boolean visitWhile(While node) { + List list = mNodeTypeDetectors.get(While.class); + if (list != null) { + for (VisitingDetector v : list) { + v.getVisitor().visitWhile(node); + } + } + return false; + } + } + + /** Performs common AST searches for method calls and R-type-field references. + * Note that this is a specialized form of the {@link DispatchVisitor}. */ + private class DelegatingJavaVisitor extends DispatchVisitor { + private final JavaContext mContext; + private final boolean mVisitResources; + private final boolean mVisitMethods; + private final boolean mVisitConstructors; + + public DelegatingJavaVisitor(JavaContext context) { + mContext = context; + + mVisitMethods = !mMethodDetectors.isEmpty(); + mVisitConstructors = !mConstructorDetectors.isEmpty(); + mVisitResources = !mResourceFieldDetectors.isEmpty(); + } + + @Override + public boolean visitSelect(Select node) { + if (mVisitResources) { + // R.type.name + if (node.astOperand() instanceof Select) { + Select select = (Select) node.astOperand(); + if (select.astOperand() instanceof VariableReference) { + VariableReference reference = (VariableReference) select.astOperand(); + if (reference.astIdentifier().astValue().equals(R_CLASS)) { + String type = select.astIdentifier().astValue(); + String name = node.astIdentifier().astValue(); + + // R -could- be referenced locally and really have been + // imported as "import android.R;" in the import statements, + // but this is not recommended (and in fact there's a specific + // lint rule warning against it) + boolean isFramework = false; + + for (VisitingDetector v : mResourceFieldDetectors) { + JavaScanner detector = v.getJavaScanner(); + //noinspection ConstantConditions + detector.visitResourceReference(mContext, v.getVisitor(), + node, type, name, isFramework); + } + + return super.visitSelect(node); + } + } + } + + // Arbitrary packages -- android.R.type.name, foo.bar.R.type.name + if (node.astIdentifier().astValue().equals(R_CLASS)) { + Node parent = node.getParent(); + if (parent instanceof Select) { + Node grandParent = parent.getParent(); + if (grandParent instanceof Select) { + Select select = (Select) grandParent; + String name = select.astIdentifier().astValue(); + Expression typeOperand = select.astOperand(); + if (typeOperand instanceof Select) { + Select typeSelect = (Select) typeOperand; + String type = typeSelect.astIdentifier().astValue(); + boolean isFramework = node.astOperand().toString().equals( + ANDROID_PKG); + for (VisitingDetector v : mResourceFieldDetectors) { + JavaScanner detector = v.getJavaScanner(); + detector.visitResourceReference(mContext, v.getVisitor(), + node, type, name, isFramework); + } + } + } + } + } + } + + return super.visitSelect(node); + } + + @Override + public boolean visitMethodInvocation(MethodInvocation node) { + if (mVisitMethods) { + String methodName = node.astName().astValue(); + List list = mMethodDetectors.get(methodName); + if (list != null) { + for (VisitingDetector v : list) { + v.getJavaScanner().visitMethod(mContext, v.getVisitor(), node); + } + } + } + + return super.visitMethodInvocation(node); + } + + @Override + public boolean visitConstructorInvocation(ConstructorInvocation node) { + if (mVisitConstructors) { + TypeReference typeReference = node.astTypeReference(); + if (typeReference != null) { + TypeReferencePart last = typeReference.astParts().last(); + if (last != null) { + String name = last.astIdentifier().astValue(); + if (mConstructorSimpleNames.contains(name)) { + ResolvedNode resolved = mContext.resolve(node); + if (resolved instanceof ResolvedMethod) { + ResolvedMethod method = (ResolvedMethod) resolved; + String type = method.getContainingClass().getSignature(); + List list = mConstructorDetectors.get(type); + if (list != null) { + for (VisitingDetector v : list) { + v.getJavaScanner().visitConstructor(mContext, + v.getVisitor(), node, method); + } + } + + } + } + } + } + } + + return super.visitConstructorInvocation(node); + } + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintClient.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintClient.java new file mode 100755 index 00000000000..fcb4dfe14d3 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintClient.java @@ -0,0 +1,1053 @@ +/* + * Copyright (C) 2011 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.client.api; + +import static com.android.SdkConstants.CLASS_FOLDER; +import static com.android.SdkConstants.DOT_AAR; +import static com.android.SdkConstants.DOT_JAR; +import static com.android.SdkConstants.GEN_FOLDER; +import static com.android.SdkConstants.LIBS_FOLDER; +import static com.android.SdkConstants.RES_FOLDER; +import static com.android.SdkConstants.SRC_FOLDER; +import static com.android.tools.lint.detector.api.LintUtils.endsWith; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.repository.ResourceVisibilityLookup; +import com.android.ide.common.res2.AbstractResourceRepository; +import com.android.ide.common.res2.ResourceItem; +import com.android.prefs.AndroidLocation; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkVersionInfo; +import com.android.sdklib.repository.local.LocalSdk; +import com.android.tools.lint.detector.api.Context; +import com.android.tools.lint.detector.api.Detector; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.LintUtils; +import com.android.tools.lint.detector.api.Location; +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.utils.XmlUtils; +import com.google.common.annotations.Beta; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.common.io.Files; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import java.io.File; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Information about the tool embedding the lint analyzer. IDEs and other tools + * implementing lint support will extend this to integrate logging, displaying errors, + * etc. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public abstract class LintClient { + private static final String PROP_BIN_DIR = "com.android.tools.lint.bindir"; //$NON-NLS-1$ + + /** + * Returns a configuration for use by the given project. The configuration + * provides information about which issues are enabled, any customizations + * to the severity of an issue, etc. + *

+ * By default this method returns a {@link DefaultConfiguration}. + * + * @param project the project to obtain a configuration for + * @return a configuration, never null. + */ + public Configuration getConfiguration(@NonNull Project project) { + return DefaultConfiguration.create(this, project, null); + } + + /** + * Report the given issue. This method will only be called if the configuration + * provided by {@link #getConfiguration(Project)} has reported the corresponding + * issue as enabled and has not filtered out the issue with its + * {@link Configuration#ignore(Context,Issue,Location,String)} method. + *

+ * @param context the context used by the detector when the issue was found + * @param issue the issue that was found + * @param severity the severity of the issue + * @param location the location of the issue + * @param message the associated user message + * @param format the format of the description and location descriptions + */ + public abstract void report( + @NonNull Context context, + @NonNull Issue issue, + @NonNull Severity severity, + @Nullable Location location, + @NonNull String message, + @NonNull TextFormat format); + + /** + * Send an exception or error message (with warning severity) to the log + * + * @param exception the exception, possibly null + * @param format the error message using {@link String#format} syntax, possibly null + * (though in that case the exception should not be null) + * @param args any arguments for the format string + */ + public void log( + @Nullable Throwable exception, + @Nullable String format, + @Nullable Object... args) { + log(Severity.WARNING, exception, format, args); + } + + /** + * Send an exception or error message to the log + * + * @param severity the severity of the warning + * @param exception the exception, possibly null + * @param format the error message using {@link String#format} syntax, possibly null + * (though in that case the exception should not be null) + * @param args any arguments for the format string + */ + public abstract void log( + @NonNull Severity severity, + @Nullable Throwable exception, + @Nullable String format, + @Nullable Object... args); + + /** + * Returns a {@link XmlParser} to use to parse XML + * + * @return a new {@link XmlParser}, or null if this client does not support + * XML analysis + */ + @Nullable + public abstract XmlParser getXmlParser(); + + /** + * Returns a {@link JavaParser} to use to parse Java + * + * @param project the project to parse, if known (this can be used to look up + * the class path for type attribution etc, and it can also be used + * to more efficiently process a set of files, for example to + * perform type attribution for multiple units in a single pass) + * @return a new {@link JavaParser}, or null if this client does not + * support Java analysis + */ + @Nullable + public abstract JavaParser getJavaParser(@Nullable Project project); + + /** + * Returns an optimal detector, if applicable. By default, just returns the + * original detector, but tools can replace detectors using this hook with a version + * that takes advantage of native capabilities of the tool. + * + * @param detectorClass the class of the detector to be replaced + * @return the new detector class, or just the original detector (not null) + */ + @NonNull + public Class replaceDetector( + @NonNull Class detectorClass) { + return detectorClass; + } + + /** + * Reads the given text file and returns the content as a string + * + * @param file the file to read + * @return the string to return, never null (will be empty if there is an + * I/O error) + */ + @NonNull + public abstract String readFile(@NonNull File file); + + /** + * Reads the given binary file and returns the content as a byte array. + * By default this method will read the bytes from the file directly, + * but this can be customized by a client if for example I/O could be + * held in memory and not flushed to disk yet. + * + * @param file the file to read + * @return the bytes in the file, never null + * @throws IOException if the file does not exist, or if the file cannot be + * read for some reason + */ + @NonNull + public byte[] readBytes(@NonNull File file) throws IOException { + return Files.toByteArray(file); + } + + /** + * Returns the list of source folders for Java source files + * + * @param project the project to look up Java source file locations for + * @return a list of source folders to search for .java files + */ + @NonNull + public List getJavaSourceFolders(@NonNull Project project) { + return getClassPath(project).getSourceFolders(); + } + + /** + * Returns the list of output folders for class files + * + * @param project the project to look up class file locations for + * @return a list of output folders to search for .class files + */ + @NonNull + public List getJavaClassFolders(@NonNull Project project) { + return getClassPath(project).getClassFolders(); + + } + + /** + * Returns the list of Java libraries + * + * @param project the project to look up jar dependencies for + * @return a list of jar dependencies containing .class files + */ + @NonNull + public List getJavaLibraries(@NonNull Project project) { + return getClassPath(project).getLibraries(); + } + + /** + * Returns the list of source folders for test source files + * + * @param project the project to look up test source file locations for + * @return a list of source folders to search for .java files + */ + @NonNull + public List getTestSourceFolders(@NonNull Project project) { + return getClassPath(project).getTestSourceFolders(); + } + + /** + * Returns the resource folders. + * + * @param project the project to look up the resource folder for + * @return a list of files pointing to the resource folders, possibly empty + */ + @NonNull + public List getResourceFolders(@NonNull Project project) { + File res = new File(project.getDir(), RES_FOLDER); + if (res.exists()) { + return Collections.singletonList(res); + } + + return Collections.emptyList(); + } + + /** + * Returns the {@link SdkInfo} to use for the given project. + * + * @param project the project to look up an {@link SdkInfo} for + * @return an {@link SdkInfo} for the project + */ + @NonNull + public SdkInfo getSdkInfo(@NonNull Project project) { + // By default no per-platform SDK info + return new DefaultSdkInfo(); + } + + /** + * Returns a suitable location for storing cache files. Note that the + * directory may not exist. + * + * @param create if true, attempt to create the cache dir if it does not + * exist + * @return a suitable location for storing cache files, which may be null if + * the create flag was false, or if for some reason the directory + * could not be created + */ + @Nullable + public File getCacheDir(boolean create) { + String home = System.getProperty("user.home"); + String relative = ".android" + File.separator + "cache"; //$NON-NLS-1$ //$NON-NLS-2$ + File dir = new File(home, relative); + if (create && !dir.exists()) { + if (!dir.mkdirs()) { + return null; + } + } + return dir; + } + + /** + * Returns the File corresponding to the system property or the environment variable + * for {@link #PROP_BIN_DIR}. + * This property is typically set by the SDK/tools/lint[.bat] wrapper. + * It denotes the path of the wrapper on disk. + * + * @return A new File corresponding to {@link LintClient#PROP_BIN_DIR} or null. + */ + @Nullable + private static File getLintBinDir() { + // First check the Java properties (e.g. set using "java -jar ... -Dname=value") + String path = System.getProperty(PROP_BIN_DIR); + if (path == null || path.isEmpty()) { + // If not found, check environment variables. + path = System.getenv(PROP_BIN_DIR); + } + if (path != null && !path.isEmpty()) { + File file = new File(path); + if (file.exists()) { + return file; + } + } + return null; + } + + /** + * Returns the File pointing to the user's SDK install area. This is generally + * the root directory containing the lint tool (but also platforms/ etc). + * + * @return a file pointing to the user's install area + */ + @Nullable + public File getSdkHome() { + File binDir = getLintBinDir(); + if (binDir != null) { + assert binDir.getName().equals("tools"); + + File root = binDir.getParentFile(); + if (root != null && root.isDirectory()) { + return root; + } + } + + String home = System.getenv("ANDROID_HOME"); //$NON-NLS-1$ + if (home != null) { + return new File(home); + } + + return null; + } + + /** + * Locates an SDK resource (relative to the SDK root directory). + *

+ * TODO: Consider switching to a {@link URL} return type instead. + * + * @param relativePath A relative path (using {@link File#separator} to + * separate path components) to the given resource + * @return a {@link File} pointing to the resource, or null if it does not + * exist + */ + @Nullable + public File findResource(@NonNull String relativePath) { + File top = getSdkHome(); + if (top == null) { + throw new IllegalArgumentException("Lint must be invoked with the System property " + + PROP_BIN_DIR + " pointing to the ANDROID_SDK tools directory"); + } + + File file = new File(top, relativePath); + if (file.exists()) { + return file; + } else { + return null; + } + } + + private Map mProjectInfo; + + /** + * Returns true if this project is a Gradle-based Android project + * + * @param project the project to check + * @return true if this is a Gradle-based project + */ + public boolean isGradleProject(Project project) { + // This is not an accurate test; specific LintClient implementations (e.g. + // IDEs or a gradle-integration of lint) have more context and can perform a more accurate + // check + if (new File(project.getDir(), SdkConstants.FN_BUILD_GRADLE).exists()) { + return true; + } + + File parent = project.getDir().getParentFile(); + if (parent != null && parent.getName().equals(SdkConstants.FD_SOURCES)) { + File root = parent.getParentFile(); + if (root != null && new File(root, SdkConstants.FN_BUILD_GRADLE).exists()) { + return true; + } + } + + return false; + } + + /** + * Information about class paths (sources, class files and libraries) + * usually associated with a project. + */ + protected static class ClassPathInfo { + private final List mClassFolders; + private final List mSourceFolders; + private final List mLibraries; + private final List mTestFolders; + + public ClassPathInfo( + @NonNull List sourceFolders, + @NonNull List classFolders, + @NonNull List libraries, + @NonNull List testFolders) { + mSourceFolders = sourceFolders; + mClassFolders = classFolders; + mLibraries = libraries; + mTestFolders = testFolders; + } + + @NonNull + public List getSourceFolders() { + return mSourceFolders; + } + + @NonNull + public List getClassFolders() { + return mClassFolders; + } + + @NonNull + public List getLibraries() { + return mLibraries; + } + + public List getTestSourceFolders() { + return mTestFolders; + } + } + + /** + * Considers the given project as an Eclipse project and returns class path + * information for the project - the source folder(s), the output folder and + * any libraries. + *

+ * Callers will not cache calls to this method, so if it's expensive to compute + * the classpath info, this method should perform its own caching. + * + * @param project the project to look up class path info for + * @return a class path info object, never null + */ + @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 sources = new ArrayList(2); + List classes = new ArrayList(1); + List libraries = new ArrayList(); + // No test folders in Eclipse: + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=224708 + List tests = Collections.emptyList(); + + File projectDir = project.getDir(); + File classpathFile = new File(projectDir, ".classpath"); //$NON-NLS-1$ + if (classpathFile.exists()) { + String classpathXml = readFile(classpathFile); + try { + Document document = XmlUtils.parseDocument(classpathXml, false); + NodeList tags = document.getElementsByTagName("classpathentry"); //$NON-NLS-1$ + for (int i = 0, n = tags.getLength(); i < n; i++) { + Element element = (Element) tags.item(i); + String kind = element.getAttribute("kind"); //$NON-NLS-1$ + List addTo = null; + if (kind.equals("src")) { //$NON-NLS-1$ + addTo = sources; + } else if (kind.equals("output")) { //$NON-NLS-1$ + addTo = classes; + } else if (kind.equals("lib")) { //$NON-NLS-1$ + addTo = libraries; + } + if (addTo != null) { + String path = element.getAttribute("path"); //$NON-NLS-1$ + File folder = new File(projectDir, path); + if (folder.exists()) { + addTo.add(folder); + } + } + } + } catch (Exception e) { + log(null, null); + } + } + + // Add in libraries that aren't specified in the .classpath file + File libs = new File(project.getDir(), LIBS_FOLDER); + if (libs.isDirectory()) { + File[] jars = libs.listFiles(); + if (jars != null) { + for (File jar : jars) { + if (LintUtils.endsWith(jar.getPath(), DOT_JAR) + && !libraries.contains(jar)) { + libraries.add(jar); + } + } + } + } + + if (classes.isEmpty()) { + File folder = new File(projectDir, CLASS_FOLDER); + if (folder.exists()) { + classes.add(folder); + } else { + // Maven checks + folder = new File(projectDir, + "target" + File.separator + "classes"); //$NON-NLS-1$ //$NON-NLS-2$ + if (folder.exists()) { + classes.add(folder); + + // If it's maven, also correct the source path, "src" works but + // it's in a more specific subfolder + if (sources.isEmpty()) { + File src = new File(projectDir, + "src" + File.separator //$NON-NLS-1$ + + "main" + File.separator //$NON-NLS-1$ + + "java"); //$NON-NLS-1$ + if (src.exists()) { + sources.add(src); + } else { + src = new File(projectDir, SRC_FOLDER); + if (src.exists()) { + sources.add(src); + } + } + + File gen = new File(projectDir, + "target" + File.separator //$NON-NLS-1$ + + "generated-sources" + File.separator //$NON-NLS-1$ + + "r"); //$NON-NLS-1$ + if (gen.exists()) { + sources.add(gen); + } + } + } + } + } + + // Fallback, in case there is no Eclipse project metadata here + if (sources.isEmpty()) { + File src = new File(projectDir, SRC_FOLDER); + if (src.exists()) { + sources.add(src); + } + File gen = new File(projectDir, GEN_FOLDER); + if (gen.exists()) { + sources.add(gen); + } + } + + info = new ClassPathInfo(sources, classes, libraries, tests); + mProjectInfo.put(project, info); + } + + return info; + } + + /** + * A map from directory to existing projects, or null. Used to ensure that + * projects are unique for a directory (in case we process a library project + * before its including project for example) + */ + private Map mDirToProject; + + /** + * Returns a project for the given directory. This should return the same + * project for the same directory if called repeatedly. + * + * @param dir the directory containing the project + * @param referenceDir See {@link Project#getReferenceDir()}. + * @return a project, never null + */ + @NonNull + public Project getProject(@NonNull File dir, @NonNull File referenceDir) { + if (mDirToProject == null) { + mDirToProject = new HashMap(); + } + + File canonicalDir = dir; + try { + // Attempt to use the canonical handle for the file, in case there + // are symlinks etc present (since when handling library projects, + // we also call getCanonicalFile to compute the result of appending + // relative paths, which can then resolve symlinks and end up with + // a different prefix) + canonicalDir = dir.getCanonicalFile(); + } catch (IOException ioe) { + // pass + } + + Project project = mDirToProject.get(canonicalDir); + if (project != null) { + return project; + } + + project = createProject(dir, referenceDir); + mDirToProject.put(canonicalDir, project); + return project; + } + + /** + * Returns the list of known projects (projects registered via + * {@link #getProject(File, File)} + * + * @return a collection of projects in any order + */ + public Collection getKnownProjects() { + return mDirToProject != null ? mDirToProject.values() : Collections.emptyList(); + } + + /** + * Registers the given project for the given directory. This can + * be used when projects are initialized outside of the client itself. + * + * @param dir the directory of the project, which must be unique + * @param project the project + */ + public void registerProject(@NonNull File dir, @NonNull Project project) { + File canonicalDir = dir; + try { + // Attempt to use the canonical handle for the file, in case there + // are symlinks etc present (since when handling library projects, + // we also call getCanonicalFile to compute the result of appending + // relative paths, which can then resolve symlinks and end up with + // a different prefix) + canonicalDir = dir.getCanonicalFile(); + } catch (IOException ioe) { + // pass + } + + + if (mDirToProject == null) { + mDirToProject = new HashMap(); + } else { + assert !mDirToProject.containsKey(dir) : dir; + } + mDirToProject.put(canonicalDir, project); + } + + private Set mProjectDirs = Sets.newHashSet(); + + /** + * Create a project for the given directory + * @param dir the root directory of the project + * @param referenceDir See {@link Project#getReferenceDir()}. + * @return a new project + */ + @NonNull + protected Project createProject(@NonNull File dir, @NonNull File referenceDir) { + if (mProjectDirs.contains(dir)) { + throw new CircularDependencyException( + "Circular library dependencies; check your project.properties files carefully"); + } + mProjectDirs.add(dir); + return Project.create(this, dir, referenceDir); + } + + /** + * Returns the name of the given project + * + * @param project the project to look up + * @return the name of the project + */ + @NonNull + public String getProjectName(@NonNull Project project) { + return project.getDir().getName(); + } + + protected IAndroidTarget[] mTargets; + + /** + * Returns all the {@link IAndroidTarget} versions installed in the user's SDK install + * area. + * + * @return all the installed targets + */ + @NonNull + public IAndroidTarget[] getTargets() { + if (mTargets == null) { + LocalSdk localSdk = getSdk(); + if (localSdk != null) { + mTargets = localSdk.getTargets(); + } else { + mTargets = new IAndroidTarget[0]; + } + } + + return mTargets; + } + + protected LocalSdk mSdk; + + /** + * Returns the SDK installation (used to look up platforms etc) + * + * @return the SDK if known + */ + @Nullable + public LocalSdk getSdk() { + if (mSdk == null) { + File sdkHome = getSdkHome(); + if (sdkHome != null) { + mSdk = new LocalSdk(sdkHome); + } + } + + return mSdk; + } + + /** + * Returns the compile target to use for the given project + * + * @param project the project in question + * + * @return the compile target to use to build the given project + */ + @Nullable + public IAndroidTarget getCompileTarget(@NonNull Project project) { + int buildSdk = project.getBuildSdk(); + IAndroidTarget[] targets = getTargets(); + for (int i = targets.length - 1; i >= 0; i--) { + IAndroidTarget target = targets[i]; + if (target.isPlatform() && target.getVersion().getApiLevel() == buildSdk) { + return target; + } + } + + return null; + } + + /** + * Returns the highest known API level. + * + * @return the highest known API level + */ + public int getHighestKnownApiLevel() { + int max = SdkVersionInfo.HIGHEST_KNOWN_STABLE_API; + + for (IAndroidTarget target : getTargets()) { + if (target.isPlatform()) { + int api = target.getVersion().getApiLevel(); + if (api > max && !target.getVersion().isPreview()) { + max = api; + } + } + } + + return max; + } + + /** + * Returns the super class for the given class name, which should be in VM + * format (e.g. java/lang/Integer, not java.lang.Integer, and using $ rather + * than . for inner classes). If the super class is not known, returns null. + *

+ * This is typically not necessary, since lint analyzes all the available + * classes. However, if this lint client is invoking lint in an incremental + * context (for example, an IDE offering incremental analysis of a single + * source file), then lint may not see all the classes, and the client can + * provide its own super class lookup. + * + * @param project the project containing the class + * @param name the fully qualified class name + * @return the corresponding super class name (in VM format), or null if not + * known + */ + @Nullable + public String getSuperClass(@NonNull Project project, @NonNull String name) { + assert name.indexOf('.') == -1 : "Use VM signatures, e.g. java/lang/Integer"; + + if ("java/lang/Object".equals(name)) { //$NON-NLS-1$ + return null; + } + + String superClass = project.getSuperClassMap().get(name); + if (superClass != null) { + return superClass; + } + + for (Project library : project.getAllLibraries()) { + superClass = library.getSuperClassMap().get(name); + if (superClass != null) { + return superClass; + } + } + + return null; + } + + /** + * Creates a super class map for the given project. The map maps from + * internal class name (e.g. java/lang/Integer, not java.lang.Integer) to its + * corresponding super class name. The root class, java/lang/Object, is not in the map. + * + * @param project the project to initialize the super class with; this will include + * local classes as well as any local .jar libraries; not transitive + * dependencies + * @return a map from class to its corresponding super class; never null + */ + @NonNull + public Map createSuperClassMap(@NonNull Project project) { + List libraries = project.getJavaLibraries(); + List classFolders = project.getJavaClassFolders(); + List classEntries = ClassEntry.fromClassPath(this, classFolders, true); + if (libraries.isEmpty()) { + return ClassEntry.createSuperClassMap(this, classEntries); + } + List libraryEntries = ClassEntry.fromClassPath(this, libraries, true); + return ClassEntry.createSuperClassMap(this, libraryEntries, classEntries); + } + + /** + * Checks whether the given name is a subclass of the given super class. If + * the method does not know, it should return null, and otherwise return + * {@link Boolean#TRUE} or {@link Boolean#FALSE}. + *

+ * Note that the class names are in internal VM format (java/lang/Integer, + * not java.lang.Integer, and using $ rather than . for inner classes). + * + * @param project the project context to look up the class in + * @param name the name of the class to be checked + * @param superClassName the name of the super class to compare to + * @return true if the class of the given name extends the given super class + */ + @SuppressWarnings("NonBooleanMethodNameMayNotStartWithQuestion") + @Nullable + public Boolean isSubclassOf( + @NonNull Project project, + @NonNull String name, + @NonNull String superClassName) { + return null; + } + + /** + * Finds any custom lint rule jars that should be included for analysis, + * regardless of project. + *

+ * The default implementation locates custom lint jars in ~/.android/lint/ and + * in $ANDROID_LINT_JARS + * + * @return a list of rule jars (possibly empty). + */ + @SuppressWarnings("MethodMayBeStatic") // Intentionally instance method so it can be overridden + @NonNull + public List findGlobalRuleJars() { + // Look for additional detectors registered by the user, via + // (1) an environment variable (useful for build servers etc), and + // (2) via jar files in the .android/lint directory + List files = null; + try { + String androidHome = AndroidLocation.getFolder(); + File lint = new File(androidHome + File.separator + "lint"); //$NON-NLS-1$ + if (lint.exists()) { + File[] list = lint.listFiles(); + if (list != null) { + for (File jarFile : list) { + if (endsWith(jarFile.getName(), DOT_JAR)) { + if (files == null) { + files = new ArrayList(); + } + files.add(jarFile); + } + } + } + } + } catch (AndroidLocation.AndroidLocationException e) { + // Ignore -- no android dir, so no rules to load. + } + + String lintClassPath = System.getenv("ANDROID_LINT_JARS"); //$NON-NLS-1$ + if (lintClassPath != null && !lintClassPath.isEmpty()) { + String[] paths = lintClassPath.split(File.pathSeparator); + for (String path : paths) { + File jarFile = new File(path); + if (jarFile.exists()) { + if (files == null) { + files = new ArrayList(); + } else if (files.contains(jarFile)) { + continue; + } + files.add(jarFile); + } + } + } + + return files != null ? files : Collections.emptyList(); + } + + /** + * Finds any custom lint rule jars that should be included for analysis + * in the given project + * + * @param project the project to look up rule jars from + * @return a list of rule jars (possibly empty). + */ + @SuppressWarnings("MethodMayBeStatic") // Intentionally instance method so it can be overridden + @NonNull + public List findRuleJars(@NonNull Project project) { + if (project.getDir().getPath().endsWith(DOT_AAR)) { + File lintJar = new File(project.getDir(), "lint.jar"); //$NON-NLS-1$ + if (lintJar.exists()) { + return Collections.singletonList(lintJar); + } + } + + return Collections.emptyList(); + } + + /** + * Opens a URL connection. + * + * Clients such as IDEs can override this to for example consider the user's IDE proxy + * settings. + * + * @param url the URL to read + * @return a {@link java.net.URLConnection} or null + * @throws IOException if any kind of IO exception occurs + */ + @Nullable + public URLConnection openConnection(@NonNull URL url) throws IOException { + return url.openConnection(); + } + + /** Closes a connection previously returned by {@link #openConnection(java.net.URL)} */ + public void closeConnection(@NonNull URLConnection connection) throws IOException { + if (connection instanceof HttpURLConnection) { + ((HttpURLConnection)connection).disconnect(); + } + } + + /** + * Returns true if the given directory is a lint project directory. + * By default, a project directory is the directory containing a manifest file, + * but in Gradle projects for example it's the root gradle directory. + * + * @param dir the directory to check + * @return true if the directory represents a lint project + */ + @SuppressWarnings("MethodMayBeStatic") // Intentionally instance method so it can be overridden + public boolean isProjectDirectory(@NonNull File dir) { + return LintUtils.isManifestFolder(dir) || Project.isAospFrameworksProject(dir); + } + + /** + * Returns whether lint should look for suppress comments. Tools that already do + * this on their own can return false here to avoid doing unnecessary work. + */ + public boolean checkForSuppressComments() { + return true; + } + + /** + * Adds in any custom lint rules and returns the result as a new issue registry, + * or the same one if no custom rules were found + * + * @param registry the main registry to add rules to + * @return a new registry containing the passed in rules plus any custom rules, + * or the original registry if no custom rules were found + */ + public IssueRegistry addCustomLintRules(@NonNull IssueRegistry registry) { + List jarFiles = findGlobalRuleJars(); + + if (!jarFiles.isEmpty()) { + List registries = Lists.newArrayListWithExpectedSize(jarFiles.size()); + registries.add(registry); + for (File jarFile : jarFiles) { + try { + registries.add(JarFileIssueRegistry.get(this, jarFile)); + } catch (Throwable e) { + log(e, "Could not load custom rule jar file %1$s", jarFile); + } + } + if (registries.size() > 1) { // the first item is the passed in registry itself + return new CompositeIssueRegistry(registries); + } + } + + return registry; + } + + /** + * Returns true if this client supports project resource repository lookup via + * {@link #getProjectResources(Project,boolean)} + * + * @return true if the client can provide project resources + */ + public boolean supportsProjectResources() { + return false; + } + + /** + * Returns the project resources, if available + * + * @param includeDependencies if true, include merged view of all dependencies + * @return the project resources, or null if not available + */ + @Nullable + public AbstractResourceRepository getProjectResources(Project project, + boolean includeDependencies) { + return null; + } + + /** + * For a lint client which supports resource items (via {@link #supportsProjectResources()}) + * return a handle for a resource item + * + * @param item the resource item to look up a location handle for + * @return a corresponding handle + */ + @NonNull + public Location.Handle createResourceItemHandle(@NonNull ResourceItem item) { + return new Location.ResourceItemHandle(item); + } + + private ResourceVisibilityLookup.Provider mResourceVisibility; + + /** + * Returns a shared {@link ResourceVisibilityLookup.Provider} + * + * @return a shared provider for looking up resource visibility + */ + @NonNull + public ResourceVisibilityLookup.Provider getResourceVisibilityProvider() { + if (mResourceVisibility == null) { + mResourceVisibility = new ResourceVisibilityLookup.Provider(); + } + return mResourceVisibility; + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintDriver.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintDriver.java new file mode 100644 index 00000000000..d2482360145 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintDriver.java @@ -0,0 +1,2535 @@ +/* + * Copyright (C) 2011 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.client.api; + +import static com.android.SdkConstants.ATTR_IGNORE; +import static com.android.SdkConstants.CLASS_CONSTRUCTOR; +import static com.android.SdkConstants.CONSTRUCTOR_NAME; +import static com.android.SdkConstants.DOT_CLASS; +import static com.android.SdkConstants.DOT_JAR; +import static com.android.SdkConstants.DOT_JAVA; +import static com.android.SdkConstants.FD_GRADLE_WRAPPER; +import static com.android.SdkConstants.FN_GRADLE_WRAPPER_PROPERTIES; +import static com.android.SdkConstants.FN_LOCAL_PROPERTIES; +import static com.android.SdkConstants.RES_FOLDER; +import static com.android.SdkConstants.SUPPRESS_ALL; +import static com.android.SdkConstants.SUPPRESS_LINT; +import static com.android.SdkConstants.TOOLS_URI; +import static com.android.ide.common.resources.configuration.FolderConfiguration.QUALIFIER_SPLITTER; +import static com.android.tools.lint.detector.api.LintUtils.isAnonymousClass; +import static java.io.File.separator; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.res2.AbstractResourceRepository; +import com.android.ide.common.res2.ResourceItem; +import com.android.resources.ResourceFolderType; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.repository.local.LocalSdk; +import com.android.tools.lint.client.api.LintListener.EventType; +import com.android.tools.lint.detector.api.ClassContext; +import com.android.tools.lint.detector.api.Context; +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.Project; +import com.android.tools.lint.detector.api.ResourceContext; +import com.android.tools.lint.detector.api.ResourceXmlDetector; +import com.android.tools.lint.detector.api.Scope; +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.google.common.annotations.Beta; +import com.google.common.base.Objects; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import lombok.ast.Annotation; +import lombok.ast.AnnotationElement; +import lombok.ast.AnnotationValue; +import lombok.ast.ArrayInitializer; +import lombok.ast.ClassDeclaration; +import lombok.ast.ConstructorDeclaration; +import lombok.ast.Expression; +import lombok.ast.MethodDeclaration; +import lombok.ast.Modifiers; +import lombok.ast.Node; +import lombok.ast.StrictListAccessor; +import lombok.ast.StringLiteral; +import lombok.ast.TypeReference; +import lombok.ast.VariableDefinition; + +/** + * Analyzes Android projects and files + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public class LintDriver { + /** + * Max number of passes to run through the lint runner if requested by + * {@link #requestRepeat} + */ + private static final int MAX_PHASES = 3; + private static final String SUPPRESS_LINT_VMSIG = '/' + SUPPRESS_LINT + ';'; + /** Prefix used by the comment suppress mechanism in Studio/IntelliJ */ + private static final String STUDIO_ID_PREFIX = "AndroidLint"; + + private final LintClient mClient; + private LintRequest mRequest; + private IssueRegistry mRegistry; + private volatile boolean mCanceled; + private EnumSet mScope; + private List mApplicableDetectors; + private Map> mScopeDetectors; + private List mListeners; + private int mPhase; + private List mRepeatingDetectors; + private EnumSet mRepeatScope; + private Project[] mCurrentProjects; + private Project mCurrentProject; + private boolean mAbbreviating = true; + private boolean mParserErrors; + private Map mProperties; + + /** + * Creates a new {@link LintDriver} + * + * @param registry The registry containing issues to be checked + * @param client the tool wrapping the analyzer, such as an IDE or a CLI + */ + public LintDriver(@NonNull IssueRegistry registry, @NonNull LintClient client) { + mRegistry = registry; + mClient = new LintClientWrapper(client); + } + + /** Cancels the current lint run as soon as possible */ + public void cancel() { + mCanceled = true; + } + + /** + * Returns the scope for the lint job + * + * @return the scope, never null + */ + @NonNull + public EnumSet getScope() { + return mScope; + } + + /** + * Sets the scope for the lint job + * + * @param scope the scope to use + */ + public void setScope(@NonNull EnumSet scope) { + mScope = scope; + } + + /** + * Returns the lint client requesting the lint check. This may not be the same + * instance as the one passed in to this driver; lint uses a wrapper which performs + * additional validation to ensure that for example badly behaved detectors which report + * issues that have been disabled will get muted without the real lint client getting + * notified. Thus, this {@link LintClient} is suitable for use by detectors to look + * up a client to for example get location handles from, but tool handling code should + * never try to cast this client back to their original lint client. For the original + * lint client, use {@link LintRequest} instead. + * + * @return the client, never null + */ + @NonNull + public LintClient getClient() { + return mClient; + } + + /** + * Returns the current request, which points to the original files to be checked, + * the original scope, the original {@link LintClient}, as well as the release mode. + * + * @return the request + */ + @NonNull + public LintRequest getRequest() { + return mRequest; + } + + /** + * Records a property for later retrieval by {@link #getProperty(Object)} + * + * @param key the key to associate the value with + * @param value the value, or null to remove a previous binding + */ + public void putProperty(@NonNull Object key, @Nullable Object value) { + if (mProperties == null) { + mProperties = Maps.newHashMap(); + } + if (value == null) { + mProperties.remove(key); + } else { + mProperties.put(key, value); + } + } + + /** + * Returns the property previously stored with the given key, or null + * + * @param key the key + * @return the value or null if not found + */ + @Nullable + public Object getProperty(@NonNull Object key) { + if (mProperties != null) { + return mProperties.get(key); + } + + return null; + } + + /** + * Returns the current phase number. The first pass is numbered 1. Only one pass + * will be performed, unless a {@link Detector} calls {@link #requestRepeat}. + * + * @return the current phase, usually 1 + */ + public int getPhase() { + return mPhase; + } + + /** + * Returns the current {@link IssueRegistry}. + * + * @return the current {@link IssueRegistry} + */ + @NonNull + public IssueRegistry getRegistry() { + return mRegistry; + } + + /** + * Returns the project containing a given file, or null if not found. This searches + * only among the currently checked project and its library projects, not among all + * possible projects being scanned sequentially. + * + * @param file the file to be checked + * @return the corresponding project, or null if not found + */ + @Nullable + public Project findProjectFor(@NonNull File file) { + if (mCurrentProjects != null) { + if (mCurrentProjects.length == 1) { + return mCurrentProjects[0]; + } + String path = file.getPath(); + for (Project project : mCurrentProjects) { + if (path.startsWith(project.getDir().getPath())) { + return project; + } + } + } + + return null; + } + + /** + * Sets whether lint should abbreviate output when appropriate. + * + * @param abbreviating true to abbreviate output, false to include everything + */ + public void setAbbreviating(boolean abbreviating) { + mAbbreviating = abbreviating; + } + + /** + * Returns whether lint should abbreviate output when appropriate. + * + * @return true if lint should abbreviate output, false when including everything + */ + public boolean isAbbreviating() { + return mAbbreviating; + } + + /** + * Returns whether lint has encountered any files with fatal parser errors + * (e.g. broken source code, or even broken parsers) + *

+ * This is useful for checks that need to make sure they've seen all data in + * order to be conclusive (such as an unused resource check). + * + * @return true if any files were not properly processed because they + * contained parser errors + */ + public boolean hasParserErrors() { + return mParserErrors; + } + + /** + * Sets whether lint has encountered files with fatal parser errors. + * + * @see #hasParserErrors() + * @param hasErrors whether parser errors have been encountered + */ + public void setHasParserErrors(boolean hasErrors) { + mParserErrors = hasErrors; + } + + /** + * Returns the projects being analyzed + * + * @return the projects being analyzed + */ + @NonNull + public List getProjects() { + if (mCurrentProjects != null) { + return Arrays.asList(mCurrentProjects); + } + return Collections.emptyList(); + } + + /** + * Analyze the given file (which can point to an Android project). Issues found + * are reported to the associated {@link LintClient}. + * + * @param files the files and directories to be analyzed + * @param scope the scope of the analysis; detectors with a wider scope will + * not be run. If null, the scope will be inferred from the files. + * @deprecated use {@link #analyze(LintRequest) instead} + */ + @Deprecated + public void analyze(@NonNull List files, @Nullable EnumSet scope) { + analyze(new LintRequest(mClient, files).setScope(scope)); + } + + /** + * Analyze the given files (which can point to Android projects or directories + * containing Android projects). Issues found are reported to the associated + * {@link LintClient}. + *

+ * Note that the {@link LintDriver} is not multi thread safe or re-entrant; + * if you want to run potentially overlapping lint jobs, create a separate driver + * for each job. + * + * @param request the files and directories to be analyzed + */ + public void analyze(@NonNull LintRequest request) { + try { + mRequest = request; + analyze(); + } finally { + mRequest = null; + } + } + + /** Runs the driver to analyze the requested files */ + private void analyze() { + mCanceled = false; + mScope = mRequest.getScope(); + assert mScope == null || !mScope.contains(Scope.ALL_RESOURCE_FILES) || + mScope.contains(Scope.RESOURCE_FILE); + + Collection projects; + try { + projects = mRequest.getProjects(); + if (projects == null) { + projects = computeProjects(mRequest.getFiles()); + } + } catch (CircularDependencyException e) { + mCurrentProject = e.getProject(); + if (mCurrentProject != null) { + Location location = e.getLocation(); + File file = location != null ? location.getFile() : mCurrentProject.getDir(); + Context context = new Context(this, mCurrentProject, null, file); + context.report(IssueRegistry.LINT_ERROR, e.getLocation(), e.getMessage()); + mCurrentProject = null; + } + return; + } + if (projects.isEmpty()) { + mClient.log(null, "No projects found for %1$s", mRequest.getFiles().toString()); + return; + } + if (mCanceled) { + return; + } + + registerCustomRules(projects); + + if (mScope == null) { + mScope = Scope.infer(projects); + } + + fireEvent(EventType.STARTING, null); + + for (Project project : projects) { + mPhase = 1; + + Project main = mRequest.getMainProject(project); + + // The set of available detectors varies between projects + computeDetectors(project); + + if (mApplicableDetectors.isEmpty()) { + // No detectors enabled in this project: skip it + continue; + } + + checkProject(project, main); + if (mCanceled) { + break; + } + + runExtraPhases(project, main); + } + + fireEvent(mCanceled ? EventType.CANCELED : EventType.COMPLETED, null); + } + + private void registerCustomRules(Collection projects) { + // Look at the various projects, and if any of them provide a custom + // lint jar, "add" them (this will replace the issue registry with + // a CompositeIssueRegistry containing the original issue registry + // plus JarFileIssueRegistry instances for each lint jar + Set jarFiles = Sets.newHashSet(); + for (Project project : projects) { + jarFiles.addAll(mClient.findRuleJars(project)); + } + + jarFiles.addAll(mClient.findGlobalRuleJars()); + + if (!jarFiles.isEmpty()) { + List registries = Lists.newArrayListWithExpectedSize(jarFiles.size()); + registries.add(mRegistry); + for (File jarFile : jarFiles) { + try { + registries.add(JarFileIssueRegistry.get(mClient, jarFile)); + } catch (Throwable e) { + mClient.log(e, "Could not load custom rule jar file %1$s", jarFile); + } + } + if (registries.size() > 1) { // the first item is mRegistry itself + mRegistry = new CompositeIssueRegistry(registries); + } + } + } + + private void runExtraPhases(@NonNull Project project, @NonNull Project main) { + // Did any detectors request another phase? + if (mRepeatingDetectors != null) { + // Yes. Iterate up to MAX_PHASES times. + + // During the extra phases, we might be narrowing the scope, and setting it in the + // scope field such that detectors asking about the available scope will get the + // correct result. However, we need to restore it to the original scope when this + // is done in case there are other projects that will be checked after this, since + // the repeated phases is done *per project*, not after all projects have been + // processed. + EnumSet oldScope = mScope; + + do { + mPhase++; + fireEvent(EventType.NEW_PHASE, + new Context(this, project, null, project.getDir())); + + // Narrow the scope down to the set of scopes requested by + // the rules. + if (mRepeatScope == null) { + mRepeatScope = Scope.ALL; + } + mScope = Scope.intersect(mScope, mRepeatScope); + if (mScope.isEmpty()) { + break; + } + + // Compute the detectors to use for this pass. + // Unlike the normal computeDetectors(project) call, + // this is going to use the existing instances, and include + // those that apply for the configuration. + computeRepeatingDetectors(mRepeatingDetectors, project); + + if (mApplicableDetectors.isEmpty()) { + // No detectors enabled in this project: skip it + continue; + } + + checkProject(project, main); + if (mCanceled) { + break; + } + } while (mPhase < MAX_PHASES && mRepeatingDetectors != null); + + mScope = oldScope; + } + } + + private void computeRepeatingDetectors(List detectors, Project project) { + // Ensure that the current visitor is recomputed + mCurrentFolderType = null; + mCurrentVisitor = null; + mCurrentXmlDetectors = null; + mCurrentBinaryDetectors = null; + + // Create map from detector class to issue such that we can + // compute applicable issues for each detector in the list of detectors + // to be repeated + List issues = mRegistry.getIssues(); + Multimap, Issue> issueMap = + ArrayListMultimap.create(issues.size(), 3); + for (Issue issue : issues) { + issueMap.put(issue.getImplementation().getDetectorClass(), issue); + } + + Map, EnumSet> detectorToScope = + new HashMap, EnumSet>(); + Map> scopeToDetectors = + new EnumMap>(Scope.class); + + List detectorList = new ArrayList(); + // Compute the list of detectors (narrowed down from mRepeatingDetectors), + // and simultaneously build up the detectorToScope map which tracks + // the scopes each detector is affected by (this is used to populate + // the mScopeDetectors map which is used during iteration). + Configuration configuration = project.getConfiguration(); + for (Detector detector : detectors) { + Class detectorClass = detector.getClass(); + Collection detectorIssues = issueMap.get(detectorClass); + if (detectorIssues != null) { + boolean add = false; + for (Issue issue : detectorIssues) { + // The reason we have to check whether the detector is enabled + // is that this is a per-project property, so when running lint in multiple + // projects, a detector enabled only in a different project could have + // requested another phase, and we end up in this project checking whether + // the detector is enabled here. + if (!configuration.isEnabled(issue)) { + continue; + } + + add = true; // Include detector if any of its issues are enabled + + EnumSet s = detectorToScope.get(detectorClass); + EnumSet issueScope = issue.getImplementation().getScope(); + if (s == null) { + detectorToScope.put(detectorClass, issueScope); + } else if (!s.containsAll(issueScope)) { + EnumSet union = EnumSet.copyOf(s); + union.addAll(issueScope); + detectorToScope.put(detectorClass, union); + } + } + + if (add) { + detectorList.add(detector); + EnumSet union = detectorToScope.get(detector.getClass()); + for (Scope s : union) { + List list = scopeToDetectors.get(s); + if (list == null) { + list = new ArrayList(); + scopeToDetectors.put(s, list); + } + list.add(detector); + } + } + } + } + + mApplicableDetectors = detectorList; + mScopeDetectors = scopeToDetectors; + mRepeatingDetectors = null; + mRepeatScope = null; + + validateScopeList(); + } + + private void computeDetectors(@NonNull Project project) { + // Ensure that the current visitor is recomputed + mCurrentFolderType = null; + mCurrentVisitor = null; + + Configuration configuration = project.getConfiguration(); + mScopeDetectors = new EnumMap>(Scope.class); + mApplicableDetectors = mRegistry.createDetectors(mClient, configuration, + mScope, mScopeDetectors); + + validateScopeList(); + } + + /** Development diagnostics only, run with assertions on */ + @SuppressWarnings("all") // Turn off warnings for the intentional assertion side effect below + private void validateScopeList() { + boolean assertionsEnabled = false; + assert assertionsEnabled = true; // Intentional side-effect + if (assertionsEnabled) { + List resourceFileDetectors = mScopeDetectors.get(Scope.RESOURCE_FILE); + if (resourceFileDetectors != null) { + for (Detector detector : resourceFileDetectors) { + assert detector instanceof ResourceXmlDetector : detector; + } + } + + List manifestDetectors = mScopeDetectors.get(Scope.MANIFEST); + if (manifestDetectors != null) { + for (Detector detector : manifestDetectors) { + assert detector instanceof Detector.XmlScanner : detector; + } + } + List javaCodeDetectors = mScopeDetectors.get(Scope.ALL_JAVA_FILES); + if (javaCodeDetectors != null) { + for (Detector detector : javaCodeDetectors) { + assert detector instanceof Detector.JavaScanner : detector; + } + } + List javaFileDetectors = mScopeDetectors.get(Scope.JAVA_FILE); + if (javaFileDetectors != null) { + for (Detector detector : javaFileDetectors) { + assert detector instanceof Detector.JavaScanner : detector; + } + } + + List classDetectors = mScopeDetectors.get(Scope.CLASS_FILE); + if (classDetectors != null) { + for (Detector detector : classDetectors) { + assert detector instanceof Detector.ClassScanner : detector; + } + } + + List classCodeDetectors = mScopeDetectors.get(Scope.ALL_CLASS_FILES); + if (classCodeDetectors != null) { + for (Detector detector : classCodeDetectors) { + assert detector instanceof Detector.ClassScanner : detector; + } + } + + List gradleDetectors = mScopeDetectors.get(Scope.GRADLE_FILE); + if (gradleDetectors != null) { + for (Detector detector : gradleDetectors) { + assert detector instanceof Detector.GradleScanner : detector; + } + } + + List otherDetectors = mScopeDetectors.get(Scope.OTHER); + if (otherDetectors != null) { + for (Detector detector : otherDetectors) { + assert detector instanceof Detector.OtherFileScanner : detector; + } + } + + List dirDetectors = mScopeDetectors.get(Scope.RESOURCE_FOLDER); + if (dirDetectors != null) { + for (Detector detector : dirDetectors) { + assert detector instanceof Detector.ResourceFolderScanner : detector; + } + } + + List binaryDetectors = mScopeDetectors.get(Scope.BINARY_RESOURCE_FILE); + if (binaryDetectors != null) { + for (Detector detector : binaryDetectors) { + assert detector instanceof Detector.BinaryResourceScanner : detector; + } + } + } + } + + private void registerProjectFile( + @NonNull Map fileToProject, + @NonNull File file, + @NonNull File projectDir, + @NonNull File rootDir) { + fileToProject.put(file, mClient.getProject(projectDir, rootDir)); + } + + private Collection computeProjects(@NonNull List files) { + // Compute list of projects + Map fileToProject = new LinkedHashMap(); + + File sharedRoot = null; + + // Ensure that we have absolute paths such that if you lint + // "foo bar" in "baz" we can show baz/ as the root + if (files.size() > 1) { + List absolute = new ArrayList(files.size()); + for (File file : files) { + absolute.add(file.getAbsoluteFile()); + } + files = absolute; + + sharedRoot = LintUtils.getCommonParent(files); + if (sharedRoot != null && sharedRoot.getParentFile() == null) { // "/" ? + sharedRoot = null; + } + } + + for (File file : files) { + if (file.isDirectory()) { + File rootDir = sharedRoot; + if (rootDir == null) { + rootDir = file; + if (files.size() > 1) { + rootDir = file.getParentFile(); + if (rootDir == null) { + rootDir = file; + } + } + } + + // Figure out what to do with a directory. Note that the meaning of the + // directory can be ambiguous: + // If you pass a directory which is unknown, we don't know if we should + // search upwards (in case you're pointing at a deep java package folder + // within the project), or if you're pointing at some top level directory + // containing lots of projects you want to scan. We attempt to do the + // right thing, which is to see if you're pointing right at a project or + // right within it (say at the src/ or res/) folder, and if not, you're + // hopefully pointing at a project tree that you want to scan recursively. + if (mClient.isProjectDirectory(file)) { + registerProjectFile(fileToProject, file, file, rootDir); + continue; + } else { + File parent = file.getParentFile(); + if (parent != null) { + if (mClient.isProjectDirectory(parent)) { + registerProjectFile(fileToProject, file, parent, parent); + continue; + } else { + parent = parent.getParentFile(); + if (parent != null && mClient.isProjectDirectory(parent)) { + registerProjectFile(fileToProject, file, parent, parent); + continue; + } + } + } + + // Search downwards for nested projects + addProjects(file, fileToProject, rootDir); + } + } else { + // Pointed at a file: Search upwards for the containing project + File parent = file.getParentFile(); + while (parent != null) { + if (mClient.isProjectDirectory(parent)) { + registerProjectFile(fileToProject, file, parent, parent); + break; + } + parent = parent.getParentFile(); + } + } + + if (mCanceled) { + return Collections.emptySet(); + } + } + + for (Map.Entry entry : fileToProject.entrySet()) { + File file = entry.getKey(); + Project project = entry.getValue(); + if (!file.equals(project.getDir())) { + if (file.isDirectory()) { + try { + File dir = file.getCanonicalFile(); + if (dir.equals(project.getDir())) { + continue; + } + } catch (IOException ioe) { + // pass + } + } + + project.addFile(file); + } + } + + // Partition the projects up such that we only return projects that aren't + // included by other projects (e.g. because they are library projects) + + Collection allProjects = fileToProject.values(); + Set roots = new HashSet(allProjects); + for (Project project : allProjects) { + roots.removeAll(project.getAllLibraries()); + } + + // Report issues for all projects that are explicitly referenced. We need to + // do this here, since the project initialization will mark all library + // projects as no-report projects by default. + for (Project project : allProjects) { + // Report issues for all projects explicitly listed or found via a directory + // traversal -- including library projects. + project.setReportIssues(true); + } + + if (LintUtils.assertionsEnabled()) { + // Make sure that all the project directories are unique. This ensures + // that we didn't accidentally end up with different project instances + // for a library project discovered as a directory as well as one + // initialized from the library project dependency list + IdentityHashMap projects = + new IdentityHashMap(); + for (Project project : roots) { + projects.put(project, project); + for (Project library : project.getAllLibraries()) { + projects.put(library, library); + } + } + Set dirs = new HashSet(); + for (Project project : projects.keySet()) { + assert !dirs.contains(project.getDir()); + dirs.add(project.getDir()); + } + } + + return roots; + } + + private void addProjects( + @NonNull File dir, + @NonNull Map fileToProject, + @NonNull File rootDir) { + if (mCanceled) { + return; + } + + if (mClient.isProjectDirectory(dir)) { + registerProjectFile(fileToProject, dir, dir, rootDir); + } else { + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + addProjects(file, fileToProject, rootDir); + } + } + } + } + } + + private void checkProject(@NonNull Project project, @NonNull Project main) { + File projectDir = project.getDir(); + + Context projectContext = new Context(this, project, null, projectDir); + fireEvent(EventType.SCANNING_PROJECT, projectContext); + + List allLibraries = project.getAllLibraries(); + Set allProjects = new HashSet(allLibraries.size() + 1); + allProjects.add(project); + allProjects.addAll(allLibraries); + mCurrentProjects = allProjects.toArray(new Project[allProjects.size()]); + + mCurrentProject = project; + + for (Detector check : mApplicableDetectors) { + check.beforeCheckProject(projectContext); + if (mCanceled) { + return; + } + } + + assert mCurrentProject == project; + runFileDetectors(project, main); + + if (!Scope.checkSingleFile(mScope)) { + List libraries = project.getAllLibraries(); + for (Project library : libraries) { + Context libraryContext = new Context(this, library, project, projectDir); + fireEvent(EventType.SCANNING_LIBRARY_PROJECT, libraryContext); + mCurrentProject = library; + + for (Detector check : mApplicableDetectors) { + check.beforeCheckLibraryProject(libraryContext); + if (mCanceled) { + return; + } + } + assert mCurrentProject == library; + + runFileDetectors(library, main); + if (mCanceled) { + return; + } + + assert mCurrentProject == library; + + for (Detector check : mApplicableDetectors) { + check.afterCheckLibraryProject(libraryContext); + if (mCanceled) { + return; + } + } + } + } + + mCurrentProject = project; + + for (Detector check : mApplicableDetectors) { + check.afterCheckProject(projectContext); + if (mCanceled) { + return; + } + } + + if (mCanceled) { + mClient.report( + projectContext, + // Must provide an issue since API guarantees that the issue parameter + IssueRegistry.CANCELLED, + Severity.INFORMATIONAL, + null /*range*/, + "Lint canceled by user", TextFormat.RAW); + } + + mCurrentProjects = null; + } + + private void runFileDetectors(@NonNull Project project, @Nullable Project main) { + // Look up manifest information (but not for library projects) + if (project.isAndroidProject()) { + for (File manifestFile : project.getManifestFiles()) { + XmlParser parser = mClient.getXmlParser(); + if (parser != null) { + XmlContext context = new XmlContext(this, project, main, manifestFile, null, + parser); + context.document = parser.parseXml(context); + if (context.document != null) { + try { + project.readManifest(context.document); + + if ((!project.isLibrary() || (main != null + && main.isMergingManifests())) + && mScope.contains(Scope.MANIFEST)) { + List detectors = mScopeDetectors.get(Scope.MANIFEST); + if (detectors != null) { + ResourceVisitor v = new ResourceVisitor(parser, detectors, + null); + fireEvent(EventType.SCANNING_FILE, context); + v.visitFile(context, manifestFile); + } + } + } finally { + if (context.document != null) { // else: freed by XmlVisitor above + parser.dispose(context, context.document); + } + } + } + } + } + + // Process both Scope.RESOURCE_FILE and Scope.ALL_RESOURCE_FILES detectors together + // in a single pass through the resource directories. + if (mScope.contains(Scope.ALL_RESOURCE_FILES) + || mScope.contains(Scope.RESOURCE_FILE) + || mScope.contains(Scope.RESOURCE_FOLDER) + || mScope.contains(Scope.BINARY_RESOURCE_FILE)) { + List dirChecks = mScopeDetectors.get(Scope.RESOURCE_FOLDER); + List binaryChecks = mScopeDetectors.get(Scope.BINARY_RESOURCE_FILE); + List checks = union(mScopeDetectors.get(Scope.RESOURCE_FILE), + mScopeDetectors.get(Scope.ALL_RESOURCE_FILES)); + boolean haveXmlChecks = checks != null && !checks.isEmpty(); + List xmlDetectors; + if (haveXmlChecks) { + xmlDetectors = new ArrayList(checks.size()); + for (Detector detector : checks) { + if (detector instanceof ResourceXmlDetector) { + xmlDetectors.add((ResourceXmlDetector) detector); + } + } + haveXmlChecks = !xmlDetectors.isEmpty(); + } else { + xmlDetectors = Collections.emptyList(); + } + if (haveXmlChecks + || dirChecks != null && !dirChecks.isEmpty() + || binaryChecks != null && !binaryChecks.isEmpty()) { + List files = project.getSubset(); + if (files != null) { + checkIndividualResources(project, main, xmlDetectors, dirChecks, + binaryChecks, files); + } else { + List resourceFolders = project.getResourceFolders(); + if (!resourceFolders.isEmpty()) { + for (File res : resourceFolders) { + checkResFolder(project, main, res, xmlDetectors, dirChecks, + binaryChecks); + } + } + } + } + } + + if (mCanceled) { + return; + } + } + + if (mScope.contains(Scope.JAVA_FILE) || mScope.contains(Scope.ALL_JAVA_FILES)) { + List checks = union(mScopeDetectors.get(Scope.JAVA_FILE), + mScopeDetectors.get(Scope.ALL_JAVA_FILES)); + if (checks != null && !checks.isEmpty()) { + List files = project.getSubset(); + if (files != null) { + checkIndividualJavaFiles(project, main, checks, files); + } else { + List sourceFolders = project.getJavaSourceFolders(); + if (mScope.contains(Scope.TEST_SOURCES)) { + List testFolders = project.getTestSourceFolders(); + if (!testFolders.isEmpty()) { + List combined = Lists.newArrayListWithExpectedSize( + sourceFolders.size() + testFolders.size()); + combined.addAll(sourceFolders); + combined.addAll(testFolders); + sourceFolders = combined; + } + } + + checkJava(project, main, sourceFolders, checks); + + } + } + } + + if (mCanceled) { + return; + } + + if (mScope.contains(Scope.CLASS_FILE) + || mScope.contains(Scope.ALL_CLASS_FILES) + || mScope.contains(Scope.JAVA_LIBRARIES)) { + checkClasses(project, main); + } + + if (mCanceled) { + return; + } + + if (mScope.contains(Scope.GRADLE_FILE)) { + checkBuildScripts(project, main); + } + + if (mCanceled) { + return; + } + + if (mScope.contains(Scope.OTHER)) { + List checks = mScopeDetectors.get(Scope.OTHER); + if (checks != null) { + OtherFileVisitor visitor = new OtherFileVisitor(checks); + visitor.scan(this, project, main); + } + } + + if (mCanceled) { + return; + } + + if (project == main && mScope.contains(Scope.PROGUARD_FILE) && + project.isAndroidProject()) { + checkProGuard(project, main); + } + + if (project == main && mScope.contains(Scope.PROPERTY_FILE)) { + checkProperties(project, main); + } + } + + private void checkBuildScripts(Project project, Project main) { + List detectors = mScopeDetectors.get(Scope.GRADLE_FILE); + if (detectors != null) { + List files = project.getSubset(); + if (files == null) { + files = project.getGradleBuildScripts(); + } + for (File file : files) { + Context context = new Context(this, project, main, file); + fireEvent(EventType.SCANNING_FILE, context); + for (Detector detector : detectors) { + if (detector.appliesTo(context, file)) { + detector.beforeCheckFile(context); + detector.visitBuildScript(context, Maps.newHashMap()); + detector.afterCheckFile(context); + } + } + } + } + } + + private void checkProGuard(Project project, Project main) { + List detectors = mScopeDetectors.get(Scope.PROGUARD_FILE); + if (detectors != null) { + List files = project.getProguardFiles(); + for (File file : files) { + Context context = new Context(this, project, main, file); + fireEvent(EventType.SCANNING_FILE, context); + for (Detector detector : detectors) { + if (detector.appliesTo(context, file)) { + detector.beforeCheckFile(context); + detector.run(context); + detector.afterCheckFile(context); + } + } + } + } + } + + private void checkProperties(Project project, Project main) { + List detectors = mScopeDetectors.get(Scope.PROPERTY_FILE); + if (detectors != null) { + checkPropertyFile(project, main, detectors, FN_LOCAL_PROPERTIES); + checkPropertyFile(project, main, detectors, FD_GRADLE_WRAPPER + separator + + FN_GRADLE_WRAPPER_PROPERTIES); + } + } + + private void checkPropertyFile(Project project, Project main, List detectors, + String relativePath) { + File file = new File(project.getDir(), relativePath); + if (file.exists()) { + Context context = new Context(this, project, main, file); + fireEvent(EventType.SCANNING_FILE, context); + for (Detector detector : detectors) { + if (detector.appliesTo(context, file)) { + detector.beforeCheckFile(context); + detector.run(context); + detector.afterCheckFile(context); + } + } + } + } + + /** True if execution has been canceled */ + boolean isCanceled() { + return mCanceled; + } + + /** + * Returns the super class for the given class name, + * which should be in VM format (e.g. java/lang/Integer, not java.lang.Integer). + * If the super class is not known, returns null. This can happen if + * the given class is not a known class according to the project or its + * libraries, for example because it refers to one of the core libraries which + * are not analyzed by lint. + * + * @param name the fully qualified class name + * @return the corresponding super class name (in VM format), or null if not known + */ + @Nullable + public String getSuperClass(@NonNull String name) { + return mClient.getSuperClass(mCurrentProject, name); + } + + /** + * Returns true if the given class is a subclass of the given super class. + * + * @param classNode the class to check whether it is a subclass of the given + * super class name + * @param superClassName the fully qualified super class name (in VM format, + * e.g. java/lang/Integer, not java.lang.Integer. + * @return true if the given class is a subclass of the given super class + */ + public boolean isSubclassOf(@NonNull ClassNode classNode, @NonNull String superClassName) { + if (superClassName.equals(classNode.superName)) { + return true; + } + + if (mCurrentProject != null) { + Boolean isSub = mClient.isSubclassOf(mCurrentProject, classNode.name, superClassName); + if (isSub != null) { + return isSub; + } + } + + String className = classNode.name; + while (className != null) { + if (className.equals(superClassName)) { + return true; + } + className = getSuperClass(className); + } + + return false; + } + @Nullable + private static List union( + @Nullable List list1, + @Nullable List list2) { + if (list1 == null) { + return list2; + } else if (list2 == null) { + return list1; + } else { + // Use set to pick out unique detectors, since it's possible for there to be overlap, + // e.g. the DuplicateIdDetector registers both a cross-resource issue and a + // single-file issue, so it shows up on both scope lists: + Set set = new HashSet(list1.size() + list2.size()); + set.addAll(list1); + set.addAll(list2); + + return new ArrayList(set); + } + } + + /** Check the classes in this project (and if applicable, in any library projects */ + private void checkClasses(Project project, Project main) { + List files = project.getSubset(); + if (files != null) { + checkIndividualClassFiles(project, main, files); + return; + } + + // We need to read in all the classes up front such that we can initialize + // the parent chains (such that for example for a virtual dispatch, we can + // also check the super classes). + + List libraries = project.getJavaLibraries(); + List libraryEntries = ClassEntry.fromClassPath(mClient, libraries, true); + + List classFolders = project.getJavaClassFolders(); + List classEntries; + if (classFolders.isEmpty()) { + String message = String.format("No `.class` files were found in project \"%1$s\", " + + "so none of the classfile based checks could be run. " + + "Does the project need to be built first?", project.getName()); + Location location = Location.create(project.getDir()); + mClient.report(new Context(this, project, main, project.getDir()), + IssueRegistry.LINT_ERROR, + project.getConfiguration().getSeverity(IssueRegistry.LINT_ERROR), + location, message, TextFormat.RAW); + classEntries = Collections.emptyList(); + } else { + classEntries = ClassEntry.fromClassPath(mClient, classFolders, true); + } + + // Actually run the detectors. Libraries should be called before the + // main classes. + runClassDetectors(Scope.JAVA_LIBRARIES, libraryEntries, project, main); + + if (mCanceled) { + return; + } + + runClassDetectors(Scope.CLASS_FILE, classEntries, project, main); + runClassDetectors(Scope.ALL_CLASS_FILES, classEntries, project, main); + } + + private void checkIndividualClassFiles( + @NonNull Project project, + @Nullable Project main, + @NonNull List files) { + List classFiles = Lists.newArrayListWithExpectedSize(files.size()); + List classFolders = project.getJavaClassFolders(); + if (!classFolders.isEmpty()) { + for (File file : files) { + String path = file.getPath(); + if (file.isFile() && path.endsWith(DOT_CLASS)) { + classFiles.add(file); + } + } + } + + List entries = ClassEntry.fromClassFiles(mClient, classFiles, classFolders, + true); + if (!entries.isEmpty()) { + Collections.sort(entries); + runClassDetectors(Scope.CLASS_FILE, entries, project, main); + } + } + + /** + * Stack of {@link ClassNode} nodes for outer classes of the currently + * processed class, including that class itself. Populated by + * {@link #runClassDetectors(Scope, List, Project, Project)} and used by + * {@link #getOuterClassNode(ClassNode)} + */ + private Deque mOuterClasses; + + private void runClassDetectors(Scope scope, List entries, + Project project, Project main) { + if (mScope.contains(scope)) { + List classDetectors = mScopeDetectors.get(scope); + if (classDetectors != null && !classDetectors.isEmpty() && !entries.isEmpty()) { + AsmVisitor visitor = new AsmVisitor(mClient, classDetectors); + + String sourceContents = null; + String sourceName = ""; + mOuterClasses = new ArrayDeque(); + ClassEntry prev = null; + for (ClassEntry entry : entries) { + if (prev != null && prev.compareTo(entry) == 0) { + // Duplicate entries for some reason: ignore + continue; + } + prev = entry; + + ClassReader reader; + ClassNode classNode; + try { + reader = new ClassReader(entry.bytes); + classNode = new ClassNode(); + reader.accept(classNode, 0 /* flags */); + } catch (Throwable t) { + mClient.log(null, "Error processing %1$s: broken class file?", + entry.path()); + continue; + } + + ClassNode peek; + while ((peek = mOuterClasses.peek()) != null) { + if (classNode.name.startsWith(peek.name)) { + break; + } else { + mOuterClasses.pop(); + } + } + mOuterClasses.push(classNode); + + if (isSuppressed(null, classNode)) { + // Class was annotated with suppress all -- no need to look any further + continue; + } + + if (sourceContents != null) { + // Attempt to reuse the source buffer if initialized + // This means making sure that the source files + // foo/bar/MyClass and foo/bar/MyClass$Bar + // and foo/bar/MyClass$3 and foo/bar/MyClass$3$1 have the same prefix. + String newName = classNode.name; + int newRootLength = newName.indexOf('$'); + if (newRootLength == -1) { + newRootLength = newName.length(); + } + int oldRootLength = sourceName.indexOf('$'); + if (oldRootLength == -1) { + oldRootLength = sourceName.length(); + } + if (newRootLength != oldRootLength || + !sourceName.regionMatches(0, newName, 0, newRootLength)) { + sourceContents = null; + } + } + + ClassContext context = new ClassContext(this, project, main, + entry.file, entry.jarFile, entry.binDir, entry.bytes, + classNode, scope == Scope.JAVA_LIBRARIES /*fromLibrary*/, + sourceContents); + + try { + visitor.runClassDetectors(context); + } catch (Exception e) { + mClient.log(e, null); + } + + if (mCanceled) { + return; + } + + sourceContents = context.getSourceContents(false/*read*/); + sourceName = classNode.name; + } + + mOuterClasses = null; + } + } + } + + /** Returns the outer class node of the given class node + * @param classNode the inner class node + * @return the outer class node */ + public ClassNode getOuterClassNode(@NonNull ClassNode classNode) { + String outerName = classNode.outerClass; + + Iterator iterator = mOuterClasses.iterator(); + while (iterator.hasNext()) { + ClassNode node = iterator.next(); + if (outerName != null) { + if (node.name.equals(outerName)) { + return node; + } + } else if (node == classNode) { + return iterator.hasNext() ? iterator.next() : null; + } + } + + return null; + } + + /** + * Returns the {@link ClassNode} corresponding to the given type, if possible, or null + * + * @param type the fully qualified type, using JVM signatures (/ and $, not . as path + * separators) + * @param flags the ASM flags to pass to the {@link ClassReader}, normally 0 but can + * for example be {@link ClassReader#SKIP_CODE} and/oor + * {@link ClassReader#SKIP_DEBUG} + * @return the class node for the type, or null + */ + @Nullable + public ClassNode findClass(@NonNull ClassContext context, @NonNull String type, int flags) { + String relative = type.replace('/', File.separatorChar) + DOT_CLASS; + File classFile = findClassFile(context.getProject(), relative); + if (classFile != null) { + if (classFile.getPath().endsWith(DOT_JAR)) { + // TODO: Handle .jar files + return null; + } + + try { + byte[] bytes = mClient.readBytes(classFile); + ClassReader reader = new ClassReader(bytes); + ClassNode classNode = new ClassNode(); + reader.accept(classNode, flags); + + return classNode; + } catch (Throwable t) { + mClient.log(null, "Error processing %1$s: broken class file?", + classFile.getPath()); + } + } + + return null; + } + + @Nullable + private File findClassFile(@NonNull Project project, String relativePath) { + for (File root : mClient.getJavaClassFolders(project)) { + File path = new File(root, relativePath); + if (path.exists()) { + return path; + } + } + // Search in the libraries + for (File root : mClient.getJavaLibraries(project)) { + // TODO: Handle .jar files! + //if (root.getPath().endsWith(DOT_JAR)) { + //} + + File path = new File(root, relativePath); + if (path.exists()) { + return path; + } + } + + // Search dependent projects + for (Project library : project.getDirectLibraries()) { + File path = findClassFile(library, relativePath); + if (path != null) { + return path; + } + } + + return null; + } + + private void checkJava( + @NonNull Project project, + @Nullable Project main, + @NonNull List sourceFolders, + @NonNull List checks) { + JavaParser javaParser = mClient.getJavaParser(project); + if (javaParser == null) { + mClient.log(null, "No java parser provided to lint: not running Java checks"); + return; + } + + assert !checks.isEmpty(); + + // Gather all Java source files in a single pass; more efficient. + List sources = new ArrayList(100); + for (File folder : sourceFolders) { + gatherJavaFiles(folder, sources); + } + if (!sources.isEmpty()) { + JavaVisitor visitor = new JavaVisitor(javaParser, checks); + List contexts = Lists.newArrayListWithExpectedSize(sources.size()); + for (File file : sources) { + JavaContext context = new JavaContext(this, project, main, file, javaParser); + contexts.add(context); + } + + visitor.prepare(contexts); + for (JavaContext context : contexts) { + fireEvent(EventType.SCANNING_FILE, context); + visitor.visitFile(context); + if (mCanceled) { + return; + } + } + visitor.dispose(); + } + } + + private void checkIndividualJavaFiles( + @NonNull Project project, + @Nullable Project main, + @NonNull List checks, + @NonNull List files) { + + JavaParser javaParser = mClient.getJavaParser(project); + if (javaParser == null) { + mClient.log(null, "No java parser provided to lint: not running Java checks"); + return; + } + + JavaVisitor visitor = new JavaVisitor(javaParser, checks); + + List contexts = Lists.newArrayListWithExpectedSize(files.size()); + for (File file : files) { + if (file.isFile() && file.getPath().endsWith(DOT_JAVA)) { + contexts.add(new JavaContext(this, project, main, file, javaParser)); + } + } + + if (contexts.isEmpty()) { + return; + } + + visitor.prepare(contexts); + + if (mCanceled) { + return; + } + + for (JavaContext context : contexts) { + fireEvent(EventType.SCANNING_FILE, context); + visitor.visitFile(context); + if (mCanceled) { + return; + } + } + + visitor.dispose(); + } + + private static void gatherJavaFiles(@NonNull File dir, @NonNull List result) { + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isFile() && file.getName().endsWith(".java")) { //$NON-NLS-1$ + result.add(file); + } else if (file.isDirectory()) { + gatherJavaFiles(file, result); + } + } + } + } + + private ResourceFolderType mCurrentFolderType; + private List mCurrentXmlDetectors; + private List mCurrentBinaryDetectors; + private ResourceVisitor mCurrentVisitor; + + @Nullable + private ResourceVisitor getVisitor( + @NonNull ResourceFolderType type, + @NonNull List checks, + @Nullable List binaryChecks) { + if (type != mCurrentFolderType) { + mCurrentFolderType = type; + + // Determine which XML resource detectors apply to the given folder type + List applicableXmlChecks = + new ArrayList(checks.size()); + for (ResourceXmlDetector check : checks) { + if (check.appliesTo(type)) { + applicableXmlChecks.add(check); + } + } + List applicableBinaryChecks = null; + if (binaryChecks != null) { + applicableBinaryChecks = new ArrayList(binaryChecks.size()); + for (Detector check : binaryChecks) { + if (check.appliesTo(type)) { + applicableBinaryChecks.add(check); + } + } + } + + // If the list of detectors hasn't changed, then just use the current visitor! + if (mCurrentXmlDetectors != null && mCurrentXmlDetectors.equals(applicableXmlChecks) + && Objects.equal(mCurrentBinaryDetectors, applicableBinaryChecks)) { + return mCurrentVisitor; + } + + mCurrentXmlDetectors = applicableXmlChecks; + mCurrentBinaryDetectors = applicableBinaryChecks; + + if (applicableXmlChecks.isEmpty() + && (applicableBinaryChecks == null || applicableBinaryChecks.isEmpty())) { + mCurrentVisitor = null; + return null; + } + + XmlParser parser = mClient.getXmlParser(); + if (parser != null) { + mCurrentVisitor = new ResourceVisitor(parser, applicableXmlChecks, + applicableBinaryChecks); + } else { + mCurrentVisitor = null; + } + } + + return mCurrentVisitor; + } + + private void checkResFolder( + @NonNull Project project, + @Nullable Project main, + @NonNull File res, + @NonNull List xmlChecks, + @Nullable List dirChecks, + @Nullable List binaryChecks) { + File[] resourceDirs = res.listFiles(); + if (resourceDirs == null) { + return; + } + + // Sort alphabetically such that we can process related folder types at the + // same time, and to have a defined behavior such that detectors can rely on + // predictable ordering, e.g. layouts are seen before menus are seen before + // values, etc (l < m < v). + + Arrays.sort(resourceDirs); + for (File dir : resourceDirs) { + ResourceFolderType type = ResourceFolderType.getFolderType(dir.getName()); + if (type != null) { + checkResourceFolder(project, main, dir, type, xmlChecks, dirChecks, binaryChecks); + } + + if (mCanceled) { + return; + } + } + } + + private void checkResourceFolder( + @NonNull Project project, + @Nullable Project main, + @NonNull File dir, + @NonNull ResourceFolderType type, + @NonNull List xmlChecks, + @Nullable List dirChecks, + @Nullable List binaryChecks) { + + // Process the resource folder + + if (dirChecks != null && !dirChecks.isEmpty()) { + ResourceContext context = new ResourceContext(this, project, main, dir, type); + String folderName = dir.getName(); + fireEvent(EventType.SCANNING_FILE, context); + for (Detector check : dirChecks) { + if (check.appliesTo(type)) { + check.beforeCheckFile(context); + check.checkFolder(context, folderName); + check.afterCheckFile(context); + } + } + if (binaryChecks == null && xmlChecks.isEmpty()) { + return; + } + } + + File[] files = dir.listFiles(); + if (files == null || files.length <= 0) { + return; + } + + ResourceVisitor visitor = getVisitor(type, xmlChecks, binaryChecks); + if (visitor != null) { // if not, there are no applicable rules in this folder + // Process files in alphabetical order, to ensure stable output + // (for example for the duplicate resource detector) + Arrays.sort(files); + for (File file : files) { + if (LintUtils.isXmlFile(file)) { + XmlContext context = new XmlContext(this, project, main, file, type, + visitor.getParser()); + fireEvent(EventType.SCANNING_FILE, context); + visitor.visitFile(context, file); + } else if (binaryChecks != null && LintUtils.isBitmapFile(file)) { + ResourceContext context = new ResourceContext(this, project, main, file, type); + fireEvent(EventType.SCANNING_FILE, context); + visitor.visitBinaryResource(context); + } + if (mCanceled) { + return; + } + } + } + } + + /** Checks individual resources */ + private void checkIndividualResources( + @NonNull Project project, + @Nullable Project main, + @NonNull List xmlDetectors, + @Nullable List dirChecks, + @Nullable List binaryChecks, + @NonNull List files) { + for (File file : files) { + if (file.isDirectory()) { + // Is it a resource folder? + ResourceFolderType type = ResourceFolderType.getFolderType(file.getName()); + if (type != null && new File(file.getParentFile(), RES_FOLDER).exists()) { + // Yes. + checkResourceFolder(project, main, file, type, xmlDetectors, dirChecks, + binaryChecks); + } else if (file.getName().equals(RES_FOLDER)) { // Is it the res folder? + // Yes + checkResFolder(project, main, file, xmlDetectors, dirChecks, binaryChecks); + } else { + mClient.log(null, "Unexpected folder %1$s; should be project, " + + "\"res\" folder or resource folder", file.getPath()); + } + } else if (file.isFile() && LintUtils.isXmlFile(file)) { + // Yes, find out its resource type + String folderName = file.getParentFile().getName(); + ResourceFolderType type = ResourceFolderType.getFolderType(folderName); + if (type != null) { + ResourceVisitor visitor = getVisitor(type, xmlDetectors, binaryChecks); + if (visitor != null) { + XmlContext context = new XmlContext(this, project, main, file, type, + visitor.getParser()); + fireEvent(EventType.SCANNING_FILE, context); + visitor.visitFile(context, file); + } + } + } else if (binaryChecks != null && file.isFile() && LintUtils.isBitmapFile(file)) { + // Yes, find out its resource type + String folderName = file.getParentFile().getName(); + ResourceFolderType type = ResourceFolderType.getFolderType(folderName); + if (type != null) { + ResourceVisitor visitor = getVisitor(type, xmlDetectors, binaryChecks); + if (visitor != null) { + ResourceContext context = new ResourceContext(this, project, main, file, + type); + fireEvent(EventType.SCANNING_FILE, context); + visitor.visitBinaryResource(context); + if (mCanceled) { + return; + } + } + } + } + } + } + + /** + * Adds a listener to be notified of lint progress + * + * @param listener the listener to be added + */ + public void addLintListener(@NonNull LintListener listener) { + if (mListeners == null) { + mListeners = new ArrayList(1); + } + mListeners.add(listener); + } + + /** + * Removes a listener such that it is no longer notified of progress + * + * @param listener the listener to be removed + */ + public void removeLintListener(@NonNull LintListener listener) { + mListeners.remove(listener); + if (mListeners.isEmpty()) { + mListeners = null; + } + } + + /** Notifies listeners, if any, that the given event has occurred */ + private void fireEvent(@NonNull LintListener.EventType type, @Nullable Context context) { + if (mListeners != null) { + for (LintListener listener : mListeners) { + listener.update(this, type, context); + } + } + } + + /** + * Wrapper around the lint client. This sits in the middle between a + * detector calling for example {@link LintClient#report} and + * the actual embedding tool, and performs filtering etc such that detectors + * and lint clients don't have to make sure they check for ignored issues or + * filtered out warnings. + */ + private class LintClientWrapper extends LintClient { + @NonNull + private final LintClient mDelegate; + + public LintClientWrapper(@NonNull LintClient delegate) { + mDelegate = delegate; + } + + @Override + public void report( + @NonNull Context context, + @NonNull Issue issue, + @NonNull Severity severity, + @Nullable Location location, + @NonNull String message, + @NonNull TextFormat format) { + assert mCurrentProject != null; + if (!mCurrentProject.getReportIssues()) { + return; + } + + Configuration configuration = context.getConfiguration(); + if (!configuration.isEnabled(issue)) { + if (issue != IssueRegistry.PARSER_ERROR && issue != IssueRegistry.LINT_ERROR) { + mDelegate.log(null, "Incorrect detector reported disabled issue %1$s", + issue.toString()); + } + return; + } + + if (configuration.isIgnored(context, issue, location, message)) { + return; + } + + if (severity == Severity.IGNORE) { + return; + } + + mDelegate.report(context, issue, severity, location, message, format); + } + + // Everything else just delegates to the embedding lint client + + @Override + @NonNull + public Configuration getConfiguration(@NonNull Project project) { + return mDelegate.getConfiguration(project); + } + + + @Override + public void log(@NonNull Severity severity, @Nullable Throwable exception, + @Nullable String format, @Nullable Object... args) { + mDelegate.log(exception, format, args); + } + + @Override + @NonNull + public String readFile(@NonNull File file) { + return mDelegate.readFile(file); + } + + @Override + @NonNull + public byte[] readBytes(@NonNull File file) throws IOException { + return mDelegate.readBytes(file); + } + + @Override + @NonNull + public List getJavaSourceFolders(@NonNull Project project) { + return mDelegate.getJavaSourceFolders(project); + } + + @Override + @NonNull + public List getJavaClassFolders(@NonNull Project project) { + return mDelegate.getJavaClassFolders(project); + } + + @NonNull + @Override + public List getJavaLibraries(@NonNull Project project) { + return mDelegate.getJavaLibraries(project); + } + + @Override + @NonNull + public List getResourceFolders(@NonNull Project project) { + return mDelegate.getResourceFolders(project); + } + + @Override + @Nullable + public XmlParser getXmlParser() { + return mDelegate.getXmlParser(); + } + + @Override + @NonNull + public Class replaceDetector( + @NonNull Class detectorClass) { + return mDelegate.replaceDetector(detectorClass); + } + + @Override + @NonNull + public SdkInfo getSdkInfo(@NonNull Project project) { + return mDelegate.getSdkInfo(project); + } + + @Override + @NonNull + public Project getProject(@NonNull File dir, @NonNull File referenceDir) { + return mDelegate.getProject(dir, referenceDir); + } + + @Nullable + @Override + public JavaParser getJavaParser(@Nullable Project project) { + return mDelegate.getJavaParser(project); + } + + @Override + public File findResource(@NonNull String relativePath) { + return mDelegate.findResource(relativePath); + } + + @Override + @Nullable + public File getCacheDir(boolean create) { + return mDelegate.getCacheDir(create); + } + + @Override + @NonNull + protected ClassPathInfo getClassPath(@NonNull Project project) { + return mDelegate.getClassPath(project); + } + + @Override + public void log(@Nullable Throwable exception, @Nullable String format, + @Nullable Object... args) { + mDelegate.log(exception, format, args); + } + + @Override + @Nullable + public File getSdkHome() { + return mDelegate.getSdkHome(); + } + + @Override + @NonNull + public IAndroidTarget[] getTargets() { + return mDelegate.getTargets(); + } + + @Nullable + @Override + public LocalSdk getSdk() { + return mDelegate.getSdk(); + } + + @Nullable + @Override + public IAndroidTarget getCompileTarget(@NonNull Project project) { + return mDelegate.getCompileTarget(project); + } + + @Override + public int getHighestKnownApiLevel() { + return mDelegate.getHighestKnownApiLevel(); + } + + @Override + @Nullable + public String getSuperClass(@NonNull Project project, @NonNull String name) { + return mDelegate.getSuperClass(project, name); + } + + @Override + @Nullable + public Boolean isSubclassOf(@NonNull Project project, @NonNull String name, + @NonNull String superClassName) { + return mDelegate.isSubclassOf(project, name, superClassName); + } + + @Override + @NonNull + public String getProjectName(@NonNull Project project) { + return mDelegate.getProjectName(project); + } + + @Override + public boolean isGradleProject(Project project) { + return mDelegate.isGradleProject(project); + } + + @NonNull + @Override + protected Project createProject(@NonNull File dir, @NonNull File referenceDir) { + return mDelegate.createProject(dir, referenceDir); + } + + @NonNull + @Override + public List findGlobalRuleJars() { + return mDelegate.findGlobalRuleJars(); + } + + @NonNull + @Override + public List findRuleJars(@NonNull Project project) { + return mDelegate.findRuleJars(project); + } + + @Override + public boolean isProjectDirectory(@NonNull File dir) { + return mDelegate.isProjectDirectory(dir); + } + + @Override + public void registerProject(@NonNull File dir, @NonNull Project project) { + log(Severity.WARNING, null, "Too late to register projects"); + mDelegate.registerProject(dir, project); + } + + @Override + public IssueRegistry addCustomLintRules(@NonNull IssueRegistry registry) { + return mDelegate.addCustomLintRules(registry); + } + + @Override + public boolean checkForSuppressComments() { + return mDelegate.checkForSuppressComments(); + } + + @Override + public boolean supportsProjectResources() { + return mDelegate.supportsProjectResources(); + } + + @Nullable + @Override + public AbstractResourceRepository getProjectResources(Project project, + boolean includeDependencies) { + return mDelegate.getProjectResources(project, includeDependencies); + } + + @NonNull + @Override + public Location.Handle createResourceItemHandle(@NonNull ResourceItem item) { + return mDelegate.createResourceItemHandle(item); + } + + @Nullable + @Override + public URLConnection openConnection(@NonNull URL url) throws IOException { + return mDelegate.openConnection(url); + } + + @Override + public void closeConnection(@NonNull URLConnection connection) throws IOException { + mDelegate.closeConnection(connection); + } + } + + /** + * Requests another pass through the data for the given detector. This is + * typically done when a detector needs to do more expensive computation, + * but it only wants to do this once it knows that an error is + * present, or once it knows more specifically what to check for. + * + * @param detector the detector that should be included in the next pass. + * Note that the lint runner may refuse to run more than a couple + * of runs. + * @param scope the scope to be revisited. This must be a subset of the + * current scope ({@link #getScope()}, and it is just a performance hint; + * in particular, the detector should be prepared to be called on other + * scopes as well (since they may have been requested by other detectors). + * You can pall null to indicate "all". + */ + public void requestRepeat(@NonNull Detector detector, @Nullable EnumSet scope) { + if (mRepeatingDetectors == null) { + mRepeatingDetectors = new ArrayList(); + } + mRepeatingDetectors.add(detector); + + if (scope != null) { + if (mRepeatScope == null) { + mRepeatScope = scope; + } else { + mRepeatScope = EnumSet.copyOf(mRepeatScope); + mRepeatScope.addAll(scope); + } + } else { + mRepeatScope = Scope.ALL; + } + } + + // Unfortunately, ASMs nodes do not extend a common DOM node type with parent + // pointers, so we have to have multiple methods which pass in each type + // of node (class, method, field) to be checked. + + /** + * Returns whether the given issue is suppressed in the given method. + * + * @param issue the issue to be checked, or null to just check for "all" + * @param classNode the class containing the issue + * @param method the method containing the issue + * @param instruction the instruction within the method, if any + * @return true if there is a suppress annotation covering the specific + * issue on this method + */ + public boolean isSuppressed( + @Nullable Issue issue, + @NonNull ClassNode classNode, + @NonNull MethodNode method, + @Nullable AbstractInsnNode instruction) { + if (method.invisibleAnnotations != null) { + @SuppressWarnings("unchecked") + List annotations = method.invisibleAnnotations; + return isSuppressed(issue, annotations); + } + + // Initializations of fields end up placed in generated methods ( + // for members and for static fields). + if (instruction != null && method.name.charAt(0) == '<') { + AbstractInsnNode next = LintUtils.getNextInstruction(instruction); + if (next != null && next.getType() == AbstractInsnNode.FIELD_INSN) { + FieldInsnNode fieldRef = (FieldInsnNode) next; + FieldNode field = findField(classNode, fieldRef.owner, fieldRef.name); + if (field != null && isSuppressed(issue, field)) { + return true; + } + } else if (classNode.outerClass != null && classNode.outerMethod == null + && isAnonymousClass(classNode)) { + if (isSuppressed(issue, classNode)) { + return true; + } + } + } + + return false; + } + + @Nullable + private static MethodInsnNode findConstructorInvocation( + @NonNull MethodNode method, + @NonNull String className) { + InsnList nodes = method.instructions; + for (int i = 0, n = nodes.size(); i < n; i++) { + AbstractInsnNode instruction = nodes.get(i); + if (instruction.getOpcode() == Opcodes.INVOKESPECIAL) { + MethodInsnNode call = (MethodInsnNode) instruction; + if (className.equals(call.owner)) { + return call; + } + } + } + + return null; + } + + @Nullable + private FieldNode findField( + @NonNull ClassNode classNode, + @NonNull String owner, + @NonNull String name) { + ClassNode current = classNode; + while (current != null) { + if (owner.equals(current.name)) { + @SuppressWarnings("rawtypes") // ASM API + List fieldList = current.fields; + for (Object f : fieldList) { + FieldNode field = (FieldNode) f; + if (field.name.equals(name)) { + return field; + } + } + return null; + } + current = getOuterClassNode(current); + } + return null; + } + + @Nullable + private MethodNode findMethod( + @NonNull ClassNode classNode, + @NonNull String name, + boolean includeInherited) { + ClassNode current = classNode; + while (current != null) { + @SuppressWarnings("rawtypes") // ASM API + List methodList = current.methods; + for (Object f : methodList) { + MethodNode method = (MethodNode) f; + if (method.name.equals(name)) { + return method; + } + } + + if (includeInherited) { + current = getOuterClassNode(current); + } else { + break; + } + } + return null; + } + + /** + * Returns whether the given issue is suppressed for the given field. + * + * @param issue the issue to be checked, or null to just check for "all" + * @param field the field potentially annotated with a suppress annotation + * @return true if there is a suppress annotation covering the specific + * issue on this field + */ + @SuppressWarnings("MethodMayBeStatic") // API; reserve need to require driver state later + public boolean isSuppressed(@Nullable Issue issue, @NonNull FieldNode field) { + if (field.invisibleAnnotations != null) { + @SuppressWarnings("unchecked") + List annotations = field.invisibleAnnotations; + return isSuppressed(issue, annotations); + } + + return false; + } + + /** + * Returns whether the given issue is suppressed in the given class. + * + * @param issue the issue to be checked, or null to just check for "all" + * @param classNode the class containing the issue + * @return true if there is a suppress annotation covering the specific + * issue in this class + */ + public boolean isSuppressed(@Nullable Issue issue, @NonNull ClassNode classNode) { + if (classNode.invisibleAnnotations != null) { + @SuppressWarnings("unchecked") + List annotations = classNode.invisibleAnnotations; + return isSuppressed(issue, annotations); + } + + if (classNode.outerClass != null && classNode.outerMethod == null + && isAnonymousClass(classNode)) { + ClassNode outer = getOuterClassNode(classNode); + if (outer != null) { + MethodNode m = findMethod(outer, CONSTRUCTOR_NAME, false); + if (m != null) { + MethodInsnNode call = findConstructorInvocation(m, classNode.name); + if (call != null) { + if (isSuppressed(issue, outer, m, call)) { + return true; + } + } + } + m = findMethod(outer, CLASS_CONSTRUCTOR, false); + if (m != null) { + MethodInsnNode call = findConstructorInvocation(m, classNode.name); + if (call != null) { + if (isSuppressed(issue, outer, m, call)) { + return true; + } + } + } + } + } + + return false; + } + + private static boolean isSuppressed(@Nullable Issue issue, List annotations) { + for (AnnotationNode annotation : annotations) { + String desc = annotation.desc; + + // We could obey @SuppressWarnings("all") too, but no need to look for it + // because that annotation only has source retention. + + if (desc.endsWith(SUPPRESS_LINT_VMSIG)) { + if (annotation.values != null) { + for (int i = 0, n = annotation.values.size(); i < n; i += 2) { + String key = (String) annotation.values.get(i); + if (key.equals("value")) { //$NON-NLS-1$ + Object value = annotation.values.get(i + 1); + if (value instanceof String) { + String id = (String) value; + if (matches(issue, id)) { + return true; + } + } else if (value instanceof List) { + @SuppressWarnings("rawtypes") + List list = (List) value; + for (Object v : list) { + if (v instanceof String) { + String id = (String) v; + if (matches(issue, id)) { + return true; + } + } + } + } + } + } + } + } + } + + return false; + } + + private static boolean matches(@Nullable Issue issue, @NonNull String id) { + if (id.equalsIgnoreCase(SUPPRESS_ALL)) { + return true; + } + + if (issue != null) { + String issueId = issue.getId(); + if (id.equalsIgnoreCase(issueId)) { + return true; + } + if (id.startsWith(STUDIO_ID_PREFIX) + && id.regionMatches(true, STUDIO_ID_PREFIX.length(), issueId, 0, issueId.length()) + && id.substring(STUDIO_ID_PREFIX.length()).equalsIgnoreCase(issueId)) { + return true; + } + } + + return false; + } + + /** + * Returns whether the given issue is suppressed in the given parse tree node. + * + * @param context the context for the source being scanned + * @param issue the issue to be checked, or null to just check for "all" + * @param scope the AST node containing the issue + * @return true if there is a suppress annotation covering the specific + * issue in this class + */ + public boolean isSuppressed(@Nullable JavaContext context, @NonNull Issue issue, + @Nullable Node scope) { + boolean checkComments = mClient.checkForSuppressComments() && + context != null && context.containsCommentSuppress(); + while (scope != null) { + Class type = scope.getClass(); + // The Lombok AST uses a flat hierarchy of node type implementation classes + // so no need to do instanceof stuff here. + if (type == VariableDefinition.class) { + // Variable + VariableDefinition declaration = (VariableDefinition) scope; + if (isSuppressed(issue, declaration.astModifiers())) { + return true; + } + } else if (type == MethodDeclaration.class) { + // Method + // Look for annotations on the method + MethodDeclaration declaration = (MethodDeclaration) scope; + if (isSuppressed(issue, declaration.astModifiers())) { + return true; + } + } else if (type == ConstructorDeclaration.class) { + // Constructor + // Look for annotations on the method + ConstructorDeclaration declaration = (ConstructorDeclaration) scope; + if (isSuppressed(issue, declaration.astModifiers())) { + return true; + } + } else if (type == ClassDeclaration.class) { + // Class + ClassDeclaration declaration = (ClassDeclaration) scope; + if (isSuppressed(issue, declaration.astModifiers())) { + return true; + } + } + + if (checkComments && context.isSuppressedWithComment(scope, issue)) { + return true; + } + + scope = scope.getParent(); + } + + return false; + } + + /** + * Returns true if the given AST modifier has a suppress annotation for the + * given issue (which can be null to check for the "all" annotation) + * + * @param issue the issue to be checked + * @param modifiers the modifier to check + * @return true if the issue or all issues should be suppressed for this + * modifier + */ + private static boolean isSuppressed(@Nullable Issue issue, @Nullable Modifiers modifiers) { + if (modifiers == null) { + return false; + } + StrictListAccessor annotations = modifiers.astAnnotations(); + if (annotations == null) { + return false; + } + + for (Annotation annotation : annotations) { + TypeReference t = annotation.astAnnotationTypeReference(); + String typeName = t.getTypeName(); + if (typeName.endsWith(SUPPRESS_LINT) + || typeName.endsWith("SuppressWarnings")) { //$NON-NLS-1$ + StrictListAccessor values = + annotation.astElements(); + if (values != null) { + for (AnnotationElement element : values) { + AnnotationValue valueNode = element.astValue(); + if (valueNode == null) { + continue; + } + if (valueNode instanceof StringLiteral) { + StringLiteral literal = (StringLiteral) valueNode; + String value = literal.astValue(); + if (matches(issue, value)) { + return true; + } + } else if (valueNode instanceof ArrayInitializer) { + ArrayInitializer array = (ArrayInitializer) valueNode; + StrictListAccessor expressions = + array.astExpressions(); + if (expressions == null) { + continue; + } + for (Expression arrayElement : expressions) { + if (arrayElement instanceof StringLiteral) { + String value = ((StringLiteral) arrayElement).astValue(); + if (matches(issue, value)) { + return true; + } + } + } + } + } + } + } + } + + return false; + } + + /** + * Returns whether the given issue is suppressed in the given XML DOM node. + * + * @param issue the issue to be checked, or null to just check for "all" + * @param node the DOM node containing the issue + * @return true if there is a suppress annotation covering the specific + * issue in this class + */ + public boolean isSuppressed(@Nullable XmlContext context, @NonNull Issue issue, + @Nullable org.w3c.dom.Node node) { + if (node instanceof Attr) { + node = ((Attr) node).getOwnerElement(); + } + boolean checkComments = mClient.checkForSuppressComments() + && context != null && context.containsCommentSuppress(); + while (node != null) { + if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) { + Element element = (Element) node; + if (element.hasAttributeNS(TOOLS_URI, ATTR_IGNORE)) { + String ignore = element.getAttributeNS(TOOLS_URI, ATTR_IGNORE); + if (ignore.indexOf(',') == -1) { + if (matches(issue, ignore)) { + return true; + } + } else { + for (String id : ignore.split(",")) { //$NON-NLS-1$ + if (matches(issue, id)) { + return true; + } + } + } + } else if (checkComments && context.isSuppressedWithComment(node, issue)) { + return true; + } + } + + node = node.getParentNode(); + } + + return false; + } + + private File mCachedFolder = null; + private int mCachedFolderVersion = -1; + /** Pattern for version qualifiers */ + private static final Pattern VERSION_PATTERN = Pattern.compile("^v(\\d+)$"); //$NON-NLS-1$ + + /** + * Returns the folder version of the given file. For example, for the file values-v14/foo.xml, + * it returns 14. + * + * @param resourceFile the file to be checked + * @return the folder version, or -1 if no specific version was specified + */ + public int getResourceFolderVersion(@NonNull File resourceFile) { + File parent = resourceFile.getParentFile(); + if (parent == null) { + return -1; + } + if (parent.equals(mCachedFolder)) { + return mCachedFolderVersion; + } + + mCachedFolder = parent; + mCachedFolderVersion = -1; + + for (String qualifier : QUALIFIER_SPLITTER.split(parent.getName())) { + Matcher matcher = VERSION_PATTERN.matcher(qualifier); + if (matcher.matches()) { + String group = matcher.group(1); + assert group != null; + mCachedFolderVersion = Integer.parseInt(group); + break; + } + } + + return mCachedFolderVersion; + } + +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintListener.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintListener.java new file mode 100644 index 00000000000..8195d36e379 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintListener.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2011 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.client.api; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.lint.detector.api.Context; +import com.google.common.annotations.Beta; + +/** + * Interface implemented by listeners to be notified of lint events + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public interface LintListener { + /** The various types of events provided to lint listeners */ + enum EventType { + /** A lint check is about to begin */ + STARTING, + + /** Lint is about to check the given project, see {@link Context#getProject()} */ + SCANNING_PROJECT, + + /** Lint is about to check the given library project, see {@link Context#getProject()} */ + SCANNING_LIBRARY_PROJECT, + + /** Lint is about to check the given file, see {@link Context#file} */ + SCANNING_FILE, + + /** A new pass was initiated */ + NEW_PHASE, + + /** The lint check was canceled */ + CANCELED, + + /** The lint check is done */ + COMPLETED, + } + + /** + * Notifies listeners that the event of the given type has occurred. + * Additional information, such as the file being scanned, or the project + * being scanned, is available in the {@link Context} object (except for the + * {@link EventType#STARTING}, {@link EventType#CANCELED} or + * {@link EventType#COMPLETED} events which are fired outside of project + * contexts.) + * + * @param driver the driver running through the checks + * @param type the type of event that occurred + * @param context the context providing additional information + */ + void update(@NonNull LintDriver driver, @NonNull EventType type, + @Nullable Context context); +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintRequest.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintRequest.java new file mode 100644 index 00000000000..5b51fd999ed --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/LintRequest.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2013 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.client.api; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.lint.detector.api.Project; +import com.android.tools.lint.detector.api.Scope; +import com.google.common.annotations.Beta; + +import java.io.File; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; + +/** + * Information about a request to run lint + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public class LintRequest { + @NonNull + protected final LintClient mClient; + + @NonNull + protected final List mFiles; + + @Nullable + protected EnumSet mScope; + + @Nullable + protected Boolean mReleaseMode; + + @Nullable + protected Collection mProjects; + + /** + * Creates a new {@linkplain LintRequest}, to be passed to a {@link LintDriver} + * + * @param client the tool wrapping the analyzer, such as an IDE or a CLI + * @param files the set of files to check with lint. This can reference Android projects, + * or directories containing Android projects, or individual XML or Java files + * (typically for incremental IDE analysis). + */ + public LintRequest(@NonNull LintClient client, @NonNull List files) { + mClient = client; + mFiles = files; + } + + /** + * Returns the lint client requesting the lint check + * + * @return the client, never null + */ + @NonNull + public LintClient getClient() { + return mClient; + } + + /** + * Returns the set of files to check with lint. This can reference Android projects, + * or directories containing Android projects, or individual XML or Java files + * (typically for incremental IDE analysis). + * + * @return the set of files to check, should not be empty + */ + @NonNull + public List getFiles() { + return mFiles; + } + + /** + * Sets the scope to use; lint checks which require a wider scope set + * will be ignored + * + * @return the scope to use, or null to use the default + */ + @Nullable + public EnumSet getScope() { + return mScope; + } + + /** + * Sets the scope to use; lint checks which require a wider scope set + * will be ignored + * + * @param scope the scope + * @return this, for constructor chaining + */ + @NonNull + public LintRequest setScope(@Nullable EnumSet scope) { + mScope = scope; + return this; + } + + /** + * Returns {@code true} if lint is invoked as part of a release mode build, + * {@code false} if it is part of a debug mode build, and {@code null} if + * the release mode is not known + * + * @return true if this lint is running in release mode, null if not known + */ + @Nullable + public Boolean isReleaseMode() { + return mReleaseMode; + } + + /** + * Sets the release mode. Use {@code true} if lint is invoked as part of a + * release mode build, {@code false} if it is part of a debug mode build, + * and {@code null} if the release mode is not known + * + * @param releaseMode true if this lint is running in release mode, null if not known + * @return this, for constructor chaining + */ + @NonNull + public LintRequest setReleaseMode(@Nullable Boolean releaseMode) { + mReleaseMode = releaseMode; + return this; + } + + /** + * Gets the projects for the lint requests. This is optional; if not provided lint will search + * the {@link #getFiles()} directories and look for projects via {@link + * LintClient#isProjectDirectory(java.io.File)}. However, this method allows a lint client to + * set up all the projects ahead of time, and associate those projects with native resources + * (in an IDE for example, each lint project can be associated with the corresponding IDE + * project). + * + * @return a collection of projects, or null + */ + @Nullable + public Collection getProjects() { + return mProjects; + } + + /** + * Sets the projects for the lint requests. This is optional; if not provided lint will search + * the {@link #getFiles()} directories and look for projects via {@link + * LintClient#isProjectDirectory(java.io.File)}. However, this method allows a lint client to + * set up all the projects ahead of time, and associate those projects with native resources + * (in an IDE for example, each lint project can be associated with the corresponding IDE + * project). + * + * @param projects a collection of projects, or null + */ + public void setProjects(@Nullable Collection projects) { + mProjects = projects; + } + + /** + * Returns the project to be used as the main project during analysis. This is + * usually the project itself, but when you are for example analyzing a library project, + * it can be the app project using the library. + * + * @param project the project to look up the main project for + * @return the main project + */ + @NonNull + public Project getMainProject(@NonNull Project project) { + return project; + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/OtherFileVisitor.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/OtherFileVisitor.java new file mode 100644 index 00000000000..0d3becd8573 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/OtherFileVisitor.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2013 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.client.api; + +import static com.android.SdkConstants.ANDROID_MANIFEST_XML; +import static com.android.SdkConstants.DOT_CLASS; +import static com.android.SdkConstants.DOT_JAVA; +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.FD_ASSETS; +import static com.android.tools.lint.detector.api.Detector.OtherFileScanner; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.lint.detector.api.Context; +import com.android.tools.lint.detector.api.Detector; +import com.android.tools.lint.detector.api.Project; +import com.android.tools.lint.detector.api.Scope; +import com.android.utils.SdkUtils; +import com.google.common.collect.Lists; + +import java.io.File; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +/** + * Visitor for "other" files: files that aren't java sources, + * XML sources, etc -- or which should have custom handling in some + * other way. + */ +class OtherFileVisitor { + @NonNull + private final List mDetectors; + + @NonNull + private Map> mFiles = new EnumMap>(Scope.class); + + OtherFileVisitor(@NonNull List detectors) { + mDetectors = detectors; + } + + /** Analyze other files in the given project */ + void scan( + @NonNull LintDriver driver, + @NonNull Project project, + @Nullable Project main) { + // Collect all project files + File projectFolder = project.getDir(); + + EnumSet scopes = EnumSet.noneOf(Scope.class); + for (Detector detector : mDetectors) { + OtherFileScanner fileScanner = (OtherFileScanner) detector; + EnumSet applicable = fileScanner.getApplicableFiles(); + if (applicable.contains(Scope.OTHER)) { + scopes = Scope.ALL; + break; + } + scopes.addAll(applicable); + } + + List subset = project.getSubset(); + + if (scopes.contains(Scope.RESOURCE_FILE)) { + if (subset != null && !subset.isEmpty()) { + List files = new ArrayList(subset.size()); + for (File file : subset) { + if (SdkUtils.endsWith(file.getPath(), DOT_XML) && + !file.getName().equals(ANDROID_MANIFEST_XML)) { + files.add(file); + } + } + if (!files.isEmpty()) { + mFiles.put(Scope.RESOURCE_FILE, files); + } + } else { + List files = Lists.newArrayListWithExpectedSize(100); + for (File res : project.getResourceFolders()) { + collectFiles(files, res); + } + File assets = new File(projectFolder, FD_ASSETS); + if (assets.exists()) { + collectFiles(files, assets); + } + if (!files.isEmpty()) { + mFiles.put(Scope.RESOURCE_FILE, files); + } + } + } + + if (scopes.contains(Scope.JAVA_FILE)) { + if (subset != null && !subset.isEmpty()) { + List files = new ArrayList(subset.size()); + for (File file : subset) { + if (file.getPath().endsWith(DOT_JAVA)) { + files.add(file); + } + } + if (!files.isEmpty()) { + mFiles.put(Scope.JAVA_FILE, files); + } + } else { + List files = Lists.newArrayListWithExpectedSize(100); + for (File srcFolder : project.getJavaSourceFolders()) { + collectFiles(files, srcFolder); + } + if (!files.isEmpty()) { + mFiles.put(Scope.JAVA_FILE, files); + } + } + } + + if (scopes.contains(Scope.CLASS_FILE)) { + if (subset != null && !subset.isEmpty()) { + List files = new ArrayList(subset.size()); + for (File file : subset) { + if (file.getPath().endsWith(DOT_CLASS)) { + files.add(file); + } + } + if (!files.isEmpty()) { + mFiles.put(Scope.CLASS_FILE, files); + } + } else { + List files = Lists.newArrayListWithExpectedSize(100); + for (File classFolder : project.getJavaClassFolders()) { + collectFiles(files, classFolder); + } + if (!files.isEmpty()) { + mFiles.put(Scope.CLASS_FILE, files); + } + } + } + + if (scopes.contains(Scope.MANIFEST)) { + if (subset != null && !subset.isEmpty()) { + List files = new ArrayList(subset.size()); + for (File file : subset) { + if (file.getName().equals(ANDROID_MANIFEST_XML)) { + files.add(file); + } + } + if (!files.isEmpty()) { + mFiles.put(Scope.MANIFEST, files); + } + } else { + List manifestFiles = project.getManifestFiles(); + if (manifestFiles != null) { + mFiles.put(Scope.MANIFEST, manifestFiles); + } + } + } + + for (Map.Entry> entry : mFiles.entrySet()) { + Scope scope = entry.getKey(); + List files = entry.getValue(); + List applicable = new ArrayList(mDetectors.size()); + for (Detector detector : mDetectors) { + OtherFileScanner fileScanner = (OtherFileScanner) detector; + EnumSet appliesTo = fileScanner.getApplicableFiles(); + if (appliesTo.contains(Scope.OTHER) || appliesTo.contains(scope)) { + applicable.add(detector); + } + } + if (!applicable.isEmpty()) { + for (File file : files) { + Context context = new Context(driver, project, main, file); + for (Detector detector : applicable) { + detector.beforeCheckFile(context); + detector.run(context); + detector.afterCheckFile(context); + } + if (driver.isCanceled()) { + return; + } + } + } + } + } + + private static void collectFiles(List files, File file) { + if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + collectFiles(files, child); + } + } + } else { + files.add(file); + } + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/ResourceVisitor.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/ResourceVisitor.java new file mode 100644 index 00000000000..25f5e15784c --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/ResourceVisitor.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2011 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.client.api; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.lint.detector.api.Detector; +import com.android.tools.lint.detector.api.Detector.XmlScanner; +import com.android.tools.lint.detector.api.LintUtils; +import com.android.tools.lint.detector.api.ResourceContext; +import com.android.tools.lint.detector.api.XmlContext; +import com.google.common.annotations.Beta; + +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.RandomAccess; + +/** + * Specialized visitor for running detectors on resources: typically XML documents, + * but also binary resources. + *

+ * It operates in two phases: + *

    + *
  1. First, it computes a set of maps where it generates a map from each + * significant element name, and each significant attribute name, to a list + * of detectors to consult for that element or attribute name. + * The set of element names or attribute names (or both) that a detector + * is interested in is provided by the detectors themselves. + *
  2. Second, it iterates over the document a single time. For each element and + * attribute it looks up the list of interested detectors, and runs them. + *
+ * It also notifies all the detectors before and after the document is processed + * such that they can do pre- and post-processing. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +class ResourceVisitor { + private final Map> mElementToCheck = + new HashMap>(); + private final Map> mAttributeToCheck = + new HashMap>(); + private final List mDocumentDetectors = + new ArrayList(); + private final List mAllElementDetectors = + new ArrayList(); + private final List mAllAttributeDetectors = + new ArrayList(); + private final List mAllDetectors; + private final List mBinaryDetectors; + private final XmlParser mParser; + + // Really want this: + // & Detector.XmlScanner> XmlVisitor(IDomParser parser, + // T xmlDetectors) { + // but it makes client code tricky and ugly. + ResourceVisitor( + @NonNull XmlParser parser, + @NonNull List xmlDetectors, + @Nullable List binaryDetectors) { + mParser = parser; + mAllDetectors = xmlDetectors; + mBinaryDetectors = binaryDetectors; + + // TODO: Check appliesTo() for files, and find a quick way to enable/disable + // rules when running through a full project! + for (Detector detector : xmlDetectors) { + Detector.XmlScanner xmlDetector = (XmlScanner) detector; + Collection attributes = xmlDetector.getApplicableAttributes(); + if (attributes == XmlScanner.ALL) { + mAllAttributeDetectors.add(xmlDetector); + } else if (attributes != null) { + for (String attribute : attributes) { + List list = mAttributeToCheck.get(attribute); + if (list == null) { + list = new ArrayList(); + mAttributeToCheck.put(attribute, list); + } + list.add(xmlDetector); + } + } + Collection elements = xmlDetector.getApplicableElements(); + if (elements == XmlScanner.ALL) { + mAllElementDetectors.add(xmlDetector); + } else if (elements != null) { + for (String element : elements) { + List list = mElementToCheck.get(element); + if (list == null) { + list = new ArrayList(); + mElementToCheck.put(element, list); + } + list.add(xmlDetector); + } + } + + if ((attributes == null || (attributes.isEmpty() + && attributes != XmlScanner.ALL)) + && (elements == null || (elements.isEmpty() + && elements != XmlScanner.ALL))) { + mDocumentDetectors.add(xmlDetector); + } + } + } + + void visitFile(@NonNull XmlContext context, @NonNull File file) { + assert LintUtils.isXmlFile(file); + + try { + if (context.document == null) { + context.document = mParser.parseXml(context); + if (context.document == null) { + // No need to log this; the parser should be reporting + // a full warning (such as IssueRegistry#PARSER_ERROR) + // with details, location, etc. + return; + } + if (context.document.getDocumentElement() == null) { + // Ignore empty documents + return; + } + } + + for (Detector check : mAllDetectors) { + check.beforeCheckFile(context); + } + + for (Detector.XmlScanner check : mDocumentDetectors) { + check.visitDocument(context, context.document); + } + + if (!mElementToCheck.isEmpty() || !mAttributeToCheck.isEmpty() + || !mAllAttributeDetectors.isEmpty() || !mAllElementDetectors.isEmpty()) { + visitElement(context, context.document.getDocumentElement()); + } + + for (Detector check : mAllDetectors) { + check.afterCheckFile(context); + } + } finally { + if (context.document != null) { + mParser.dispose(context, context.document); + context.document = null; + } + } + } + + private void visitElement(@NonNull XmlContext context, @NonNull Element element) { + List elementChecks = mElementToCheck.get(element.getTagName()); + if (elementChecks != null) { + assert elementChecks instanceof RandomAccess; + for (XmlScanner check : elementChecks) { + check.visitElement(context, element); + } + } + if (!mAllElementDetectors.isEmpty()) { + for (XmlScanner check : mAllElementDetectors) { + check.visitElement(context, element); + } + } + + if (!mAttributeToCheck.isEmpty() || !mAllAttributeDetectors.isEmpty()) { + NamedNodeMap attributes = element.getAttributes(); + for (int i = 0, n = attributes.getLength(); i < n; i++) { + Attr attribute = (Attr) attributes.item(i); + String name = attribute.getLocalName(); + if (name == null) { + name = attribute.getName(); + } + List list = mAttributeToCheck.get(name); + if (list != null) { + for (XmlScanner check : list) { + check.visitAttribute(context, attribute); + } + } + if (!mAllAttributeDetectors.isEmpty()) { + for (XmlScanner check : mAllAttributeDetectors) { + check.visitAttribute(context, attribute); + } + } + } + } + + // Visit children + NodeList childNodes = element.getChildNodes(); + for (int i = 0, n = childNodes.getLength(); i < n; i++) { + Node child = childNodes.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + visitElement(context, (Element) child); + } + } + + // Post hooks + if (elementChecks != null) { + for (XmlScanner check : elementChecks) { + check.visitElementAfter(context, element); + } + } + if (!mAllElementDetectors.isEmpty()) { + for (XmlScanner check : mAllElementDetectors) { + check.visitElementAfter(context, element); + } + } + } + + @NonNull + public XmlParser getParser() { + return mParser; + } + + public void visitBinaryResource(@NonNull ResourceContext context) { + if (mBinaryDetectors == null) { + return; + } + for (Detector check : mBinaryDetectors) { + check.beforeCheckFile(context); + check.checkBinaryResource(context); + check.afterCheckFile(context); + } + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/SdkInfo.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/SdkInfo.java new file mode 100644 index 00000000000..8b3d1e97e89 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/SdkInfo.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2011 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.client.api; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.google.common.annotations.Beta; + +/** + * Information about SDKs + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public abstract class SdkInfo { + /** + * Returns true if the given child view is the same class or a sub class of + * the given parent view class + * + * @param parentViewFqcn the fully qualified class name of the parent view + * @param childViewFqcn the fully qualified class name of the child view + * @return true if the child view is a sub view of (or the same class as) + * the parent view + */ + public boolean isSubViewOf(@NonNull String parentViewFqcn, @NonNull String childViewFqcn) { + while (!childViewFqcn.equals("android.view.View")) { //$NON-NLS-1$ + if (parentViewFqcn.equals(childViewFqcn)) { + return true; + } + String parent = getParentViewClass(childViewFqcn); + if (parent == null) { + // Unknown view - err on the side of caution + return true; + } + childViewFqcn = parent; + } + + return false; + } + + + /** + * Returns the fully qualified name of the parent view, or null if the view + * is the root android.view.View class. + * + * @param fqcn the fully qualified class name of the view + * @return the fully qualified class name of the parent view, or null + */ + @Nullable + public abstract String getParentViewClass(@NonNull String fqcn); + + /** + * Returns the class name of the parent view, or null if the view is the + * root android.view.View class. This is the same as the + * {@link #getParentViewClass(String)} but without the package. + * + * @param name the view class name to look up the parent for (not including + * package) + * @return the view name of the parent + */ + @Nullable + public abstract String getParentViewName(@NonNull String name); + + /** + * Returns true if the given widget name is a layout + * + * @param tag the XML tag for the view + * @return true if the given tag corresponds to a layout + */ + public boolean isLayout(@NonNull String tag) { + return tag.endsWith("Layout"); //$NON-NLS-1$ + } + + // TODO: Add access to resource resolution here. +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/client/api/XmlParser.java b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/XmlParser.java new file mode 100644 index 00000000000..2660b0a8fea --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/client/api/XmlParser.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2011 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.client.api; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.lint.detector.api.Context; +import com.android.tools.lint.detector.api.Location; +import com.android.tools.lint.detector.api.XmlContext; +import com.google.common.annotations.Beta; + +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +/** + * A wrapper for an XML parser. This allows tools integrating lint to map directly + * to builtin services, such as already-parsed data structures in XML editors. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public abstract class XmlParser { + /** + * Parse the file pointed to by the given context and return as a Document + * + * @param context the context pointing to the file to be parsed, typically + * via {@link Context#getContents()} but the file handle ( + * {@link Context#file} can also be used to map to an existing + * editor buffer in the surrounding tool, etc) + * @return the parsed DOM document, or null if parsing fails + */ + @Nullable + public abstract Document parseXml(@NonNull XmlContext context); + + /** + * Returns a {@link Location} for the given DOM node + * + * @param context information about the file being parsed + * @param node the node to create a location for + * @return a location for the given node + */ + @NonNull + public abstract Location getLocation(@NonNull XmlContext context, @NonNull Node node); + + /** + * Returns a {@link Location} for the given DOM node. Like + * {@link #getLocation(XmlContext, Node)}, but allows a position range that + * is a subset of the node range. + * + * @param context information about the file being parsed + * @param node the node to create a location for + * @param start the starting position within the node, inclusive + * @param end the ending position within the node, exclusive + * @return a location for the given node + */ + @NonNull + public abstract Location getLocation(@NonNull XmlContext context, @NonNull Node node, + int start, int end); + + /** + * Returns a {@link Location} for the given DOM node + * + * @param context information about the file being parsed + * @param node the node to create a location for + * @return a location for the given node + */ + @NonNull + public abstract Location getNameLocation(@NonNull XmlContext context, @NonNull Node node); + + /** + * Returns a {@link Location} for the given DOM node + * + * @param context information about the file being parsed + * @param node the node to create a location for + * @return a location for the given node + */ + @NonNull + public abstract Location getValueLocation(@NonNull XmlContext context, @NonNull Attr node); + + /** + * Creates a light-weight handle to a location for the given node. It can be + * turned into a full fledged location by + * {@link com.android.tools.lint.detector.api.Location.Handle#resolve()}. + * + * @param context the context providing the node + * @param node the node (element or attribute) to create a location handle + * for + * @return a location handle + */ + @NonNull + public abstract Location.Handle createLocationHandle(@NonNull XmlContext context, + @NonNull Node node); + + /** + * Dispose any data structures held for the given context. + * @param context information about the file previously parsed + * @param document the document that was parsed and is now being disposed + */ + public void dispose(@NonNull XmlContext context, @NonNull Document document) { + } + + /** + * Returns the start offset of the given node, or -1 if not known + * + * @param context the context providing the node + * @param node the node (element or attribute) to create a location handle + * for + * @return the start offset, or -1 if not known + */ + public abstract int getNodeStartOffset(@NonNull XmlContext context, @NonNull Node node); + + /** + * Returns the end offset of the given node, or -1 if not known + * + * @param context the context providing the node + * @param node the node (element or attribute) to create a location handle + * for + * @return the end offset, or -1 if not known + */ + public abstract int getNodeEndOffset(@NonNull XmlContext context, @NonNull Node node); +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Category.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Category.java new file mode 100644 index 00000000000..a950634b4f3 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Category.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2011 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.detector.api; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.google.common.annotations.Beta; + +/** + * A category is a container for related issues. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public final class Category implements Comparable { + private final String mName; + private final int mPriority; + private final Category mParent; + + /** + * Creates a new {@link Category}. + * + * @param parent the name of a parent category, or null + * @param name the name of the category + * @param priority a sorting priority, with higher being more important + */ + private Category( + @Nullable Category parent, + @NonNull String name, + int priority) { + mParent = parent; + mName = name; + mPriority = priority; + } + + /** + * Creates a new top level {@link Category} with the given sorting priority. + * + * @param name the name of the category + * @param priority a sorting priority, with higher being more important + * @return a new category + */ + @NonNull + public static Category create(@NonNull String name, int priority) { + return new Category(null, name, priority); + } + + /** + * Creates a new top level {@link Category} with the given sorting priority. + * + * @param parent the name of a parent category, or null + * @param name the name of the category + * @param priority a sorting priority, with higher being more important + * @return a new category + */ + @NonNull + public static Category create(@Nullable Category parent, @NonNull String name, int priority) { + return new Category(parent, name, priority); + } + + /** + * Returns the parent category, or null if this is a top level category + * + * @return the parent category, or null if this is a top level category + */ + public Category getParent() { + return mParent; + } + + /** + * Returns the name of this category + * + * @return the name of this category + */ + public String getName() { + return mName; + } + + /** + * Returns a full name for this category. For a top level category, this is just + * the {@link #getName()} value, but for nested categories it will include the parent + * names as well. + * + * @return a full name for this category + */ + public String getFullName() { + if (mParent != null) { + return mParent.getFullName() + ':' + mName; + } else { + return mName; + } + } + + @Override + public int compareTo(Category other) { + if (other.mPriority == mPriority) { + if (mParent == other) { + return 1; + } else if (other.mParent == this) { + return -1; + } + } + return other.mPriority - mPriority; + } + + /** Issues related to running lint itself */ + public static final Category LINT = create("Lint", 110); + + /** Issues related to correctness */ + public static final Category CORRECTNESS = create("Correctness", 100); + + /** Issues related to security */ + public static final Category SECURITY = create("Security", 90); + + /** Issues related to performance */ + public static final Category PERFORMANCE = create("Performance", 80); + + /** Issues related to usability */ + public static final Category USABILITY = create("Usability", 70); + + /** Issues related to accessibility */ + public static final Category A11Y = create("Accessibility", 60); + + /** Issues related to internationalization */ + public static final Category I18N = create("Internationalization", 50); + + /** Issues related to right to left and bi-directional text support */ + public static final Category RTL = create("Bi-directional Text", 40); + + // Sub categories + + /** Issues related to icons */ + public static final Category ICONS = create(USABILITY, "Icons", 73); + + /** Issues related to typography */ + public static final Category TYPOGRAPHY = create(USABILITY, "Typography", 76); + + /** Issues related to messages/strings */ + public static final Category MESSAGES = create(CORRECTNESS, "Messages", 95); +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ClassContext.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ClassContext.java new file mode 100644 index 00000000000..2d08ed99380 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ClassContext.java @@ -0,0 +1,720 @@ +/* + * Copyright (C) 2011 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.detector.api; + +import static com.android.SdkConstants.CONSTRUCTOR_NAME; +import static com.android.SdkConstants.DOT_CLASS; +import static com.android.SdkConstants.DOT_JAVA; +import static com.android.tools.lint.detector.api.Location.SearchDirection.BACKWARD; +import static com.android.tools.lint.detector.api.Location.SearchDirection.EOL_BACKWARD; +import static com.android.tools.lint.detector.api.Location.SearchDirection.FORWARD; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.lint.client.api.LintDriver; +import com.android.tools.lint.detector.api.Location.SearchDirection; +import com.android.tools.lint.detector.api.Location.SearchHints; +import com.google.common.annotations.Beta; +import com.google.common.base.Splitter; + +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.LineNumberNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; + +import java.io.File; +import java.util.List; + +/** + * A {@link Context} used when checking .class files. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public class ClassContext extends Context { + private final File mBinDir; + /** The class file DOM root node */ + private final ClassNode mClassNode; + /** The class file byte data */ + private final byte[] mBytes; + /** The source file, if known/found */ + private File mSourceFile; + /** The contents of the source file, if source file is known/found */ + private String mSourceContents; + /** Whether we've searched for the source file (used to avoid repeated failed searches) */ + private boolean mSearchedForSource; + /** If the file is a relative path within a jar file, this is the jar file, otherwise null */ + private final File mJarFile; + /** Whether this class is part of a library (rather than corresponding to one of the + * source files in this project */ + private final boolean mFromLibrary; + + /** + * Construct a new {@link ClassContext} + * + * @param driver the driver running through the checks + * @param project the project containing the file being checked + * @param main the main project if this project is a library project, or + * null if this is not a library project. The main project is the + * root project of all library projects, not necessarily the + * directly including project. + * @param file the file being checked + * @param jarFile If the file is a relative path within a jar file, this is + * the jar file, otherwise null + * @param binDir the root binary directory containing this .class file. + * @param bytes the bytecode raw data + * @param classNode the bytecode object model + * @param fromLibrary whether this class is from a library rather than part + * of this project + * @param sourceContents initial contents of the Java source, if known, or + * null + */ + public ClassContext( + @NonNull LintDriver driver, + @NonNull Project project, + @Nullable Project main, + @NonNull File file, + @Nullable File jarFile, + @NonNull File binDir, + @NonNull byte[] bytes, + @NonNull ClassNode classNode, + boolean fromLibrary, + @Nullable String sourceContents) { + super(driver, project, main, file); + mJarFile = jarFile; + mBinDir = binDir; + mBytes = bytes; + mClassNode = classNode; + mFromLibrary = fromLibrary; + mSourceContents = sourceContents; + } + + /** + * Returns the raw bytecode data for this class file + * + * @return the byte array containing the bytecode data + */ + @NonNull + public byte[] getBytecode() { + return mBytes; + } + + /** + * Returns the bytecode object model + * + * @return the bytecode object model, never null + */ + @NonNull + public ClassNode getClassNode() { + return mClassNode; + } + + /** + * Returns the jar file, if any. If this is null, the .class file is a real file + * on disk, otherwise it represents a relative path within the jar file. + * + * @return the jar file, or null + */ + @Nullable + public File getJarFile() { + return mJarFile; + } + + /** + * Returns whether this class is part of a library (not this project). + * + * @return true if this class is part of a library + */ + public boolean isFromClassLibrary() { + return mFromLibrary; + } + + /** + * Returns the source file for this class file, if possible. + * + * @return the source file, or null + */ + @Nullable + public File getSourceFile() { + if (mSourceFile == null && !mSearchedForSource) { + mSearchedForSource = true; + + String source = mClassNode.sourceFile; + if (source == null) { + source = file.getName(); + if (source.endsWith(DOT_CLASS)) { + source = source.substring(0, source.length() - DOT_CLASS.length()) + DOT_JAVA; + } + int index = source.indexOf('$'); + if (index != -1) { + source = source.substring(0, index) + DOT_JAVA; + } + } + if (source != null) { + if (mJarFile != null) { + String relative = file.getParent() + File.separator + source; + List sources = getProject().getJavaSourceFolders(); + for (File dir : sources) { + File sourceFile = new File(dir, relative); + if (sourceFile.exists()) { + mSourceFile = sourceFile; + break; + } + } + } else { + // Determine package + String topPath = mBinDir.getPath(); + String parentPath = file.getParentFile().getPath(); + if (parentPath.startsWith(topPath)) { + int start = topPath.length() + 1; + String relative = start > parentPath.length() ? // default package? + "" : parentPath.substring(start); + List sources = getProject().getJavaSourceFolders(); + for (File dir : sources) { + File sourceFile = new File(dir, relative + File.separator + source); + if (sourceFile.exists()) { + mSourceFile = sourceFile; + break; + } + } + } + } + } + } + + return mSourceFile; + } + + /** + * Returns the contents of the source file for this class file, if found. + * + * @return the source contents, or "" + */ + @NonNull + public String getSourceContents() { + if (mSourceContents == null) { + File sourceFile = getSourceFile(); + if (sourceFile != null) { + mSourceContents = getClient().readFile(mSourceFile); + } + + if (mSourceContents == null) { + mSourceContents = ""; + } + } + + return mSourceContents; + } + + /** + * Returns the contents of the source file for this class file, if found. If + * {@code read} is false, do not read the source contents if it has not + * already been read. (This is primarily intended for the lint + * infrastructure; most client code would call {@link #getSourceContents()} + * .) + * + * @param read whether to read the source contents if it has not already + * been initialized + * @return the source contents, which will never be null if {@code read} is + * true, or null if {@code read} is false and the source contents + * hasn't already been read. + */ + @Nullable + public String getSourceContents(boolean read) { + if (read) { + return getSourceContents(); + } else { + return mSourceContents; + } + } + + /** + * Returns a location for the given source line number in this class file's + * source file, if available. + * + * @param line the line number (1-based, which is what ASM uses) + * @param patternStart optional pattern to search for in the source for + * range start + * @param patternEnd optional pattern to search for in the source for range + * end + * @param hints additional hints about the pattern search (provided + * {@code patternStart} is non null) + * @return a location, never null + */ + @NonNull + public Location getLocationForLine(int line, @Nullable String patternStart, + @Nullable String patternEnd, @Nullable SearchHints hints) { + File sourceFile = getSourceFile(); + if (sourceFile != null) { + // ASM line numbers are 1-based, and lint line numbers are 0-based + if (line != -1) { + return Location.create(sourceFile, getSourceContents(), line - 1, + patternStart, patternEnd, hints); + } else { + return Location.create(sourceFile); + } + } + + return Location.create(file); + } + + /** + * Reports an issue. + *

+ * Detectors should only call this method if an error applies to the whole class + * scope and there is no specific method or field that applies to the error. + * If so, use + * {@link #report(Issue, org.objectweb.asm.tree.MethodNode, org.objectweb.asm.tree.AbstractInsnNode, Location, String)} or + * {@link #report(Issue, org.objectweb.asm.tree.FieldNode, Location, String)}, such that + * suppress annotations are checked. + * + * @param issue the issue to report + * @param location the location of the issue, or null if not known + * @param message the message for this warning + */ + @Override + public void report( + @NonNull Issue issue, + @Nullable Location location, + @NonNull String message) { + if (mDriver.isSuppressed(issue, mClassNode)) { + return; + } + ClassNode curr = mClassNode; + while (curr != null) { + ClassNode prev = curr; + curr = mDriver.getOuterClassNode(curr); + if (curr != null) { + if (prev.outerMethod != null) { + @SuppressWarnings("rawtypes") // ASM API + List methods = curr.methods; + for (Object m : methods) { + MethodNode method = (MethodNode) m; + if (method.name.equals(prev.outerMethod) + && method.desc.equals(prev.outerMethodDesc)) { + // Found the outer method for this anonymous class; continue + // reporting on it (which will also work its way up the parent + // class hierarchy) + if (method != null && mDriver.isSuppressed(issue, mClassNode, method, + null)) { + return; + } + break; + } + } + } + if (mDriver.isSuppressed(issue, curr)) { + return; + } + } + } + + super.report(issue, location, message); + } + + // Unfortunately, ASMs nodes do not extend a common DOM node type with parent + // pointers, so we have to have multiple methods which pass in each type + // of node (class, method, field) to be checked. + + /** + * Reports an issue applicable to a given method node. + * + * @param issue the issue to report + * @param method the method scope the error applies to. The lint + * infrastructure will check whether there are suppress + * annotations on this method (or its enclosing class) and if so + * suppress the warning without involving the client. + * @param instruction the instruction within the method the error applies + * to. You cannot place annotations on individual method + * instructions (for example, annotations on local variables are + * allowed, but are not kept in the .class file). However, this + * instruction is needed to handle suppressing errors on field + * initializations; in that case, the errors may be reported in + * the {@code } method, but the annotation is found not + * on that method but for the {@link FieldNode}'s. + * @param location the location of the issue, or null if not known + * @param message the message for this warning + */ + public void report( + @NonNull Issue issue, + @Nullable MethodNode method, + @Nullable AbstractInsnNode instruction, + @Nullable Location location, + @NonNull String message) { + if (method != null && mDriver.isSuppressed(issue, mClassNode, method, instruction)) { + return; + } + report(issue, location, message); // also checks the class node + } + + /** + * Reports an issue applicable to a given method node. + * + * @param issue the issue to report + * @param field the scope the error applies to. The lint infrastructure + * will check whether there are suppress annotations on this field (or its enclosing + * class) and if so suppress the warning without involving the client. + * @param location the location of the issue, or null if not known + * @param message the message for this warning + */ + public void report( + @NonNull Issue issue, + @Nullable FieldNode field, + @Nullable Location location, + @NonNull String message) { + if (field != null && mDriver.isSuppressed(issue, field)) { + return; + } + report(issue, location, message); // also checks the class node + } + + /** + * Report an error. + * Like {@link #report(Issue, MethodNode, AbstractInsnNode, Location, String)} but with + * a now-unused data parameter at the end. + * + * @deprecated Use {@link #report(Issue, FieldNode, Location, String)} instead; + * this method is here for custom rule compatibility + */ + @SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules + @Deprecated + public void report( + @NonNull Issue issue, + @Nullable MethodNode method, + @Nullable AbstractInsnNode instruction, + @Nullable Location location, + @NonNull String message, + @SuppressWarnings("UnusedParameters") @Nullable Object data) { + report(issue, method, instruction, location, message); + } + + /** + * Report an error. + * Like {@link #report(Issue, FieldNode, Location, String)} but with + * a now-unused data parameter at the end. + * + * @deprecated Use {@link #report(Issue, FieldNode, Location, String)} instead; + * this method is here for custom rule compatibility + */ + @SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules + @Deprecated + public void report( + @NonNull Issue issue, + @Nullable FieldNode field, + @Nullable Location location, + @NonNull String message, + @SuppressWarnings("UnusedParameters") @Nullable Object data) { + report(issue, field, location, message); + } + + /** + * Finds the line number closest to the given node + * + * @param node the instruction node to get a line number for + * @return the closest line number, or -1 if not known + */ + public static int findLineNumber(@NonNull AbstractInsnNode node) { + AbstractInsnNode curr = node; + + // First search backwards + while (curr != null) { + if (curr.getType() == AbstractInsnNode.LINE) { + return ((LineNumberNode) curr).line; + } + curr = curr.getPrevious(); + } + + // Then search forwards + curr = node; + while (curr != null) { + if (curr.getType() == AbstractInsnNode.LINE) { + return ((LineNumberNode) curr).line; + } + curr = curr.getNext(); + } + + return -1; + } + + /** + * Finds the line number closest to the given method declaration + * + * @param node the method node to get a line number for + * @return the closest line number, or -1 if not known + */ + public static int findLineNumber(@NonNull MethodNode node) { + if (node.instructions != null && node.instructions.size() > 0) { + return findLineNumber(node.instructions.get(0)); + } + + return -1; + } + + /** + * Finds the line number closest to the given class declaration + * + * @param node the method node to get a line number for + * @return the closest line number, or -1 if not known + */ + public static int findLineNumber(@NonNull ClassNode node) { + if (node.methods != null && !node.methods.isEmpty()) { + MethodNode firstMethod = getFirstRealMethod(node); + if (firstMethod != null) { + return findLineNumber(firstMethod); + } + } + + return -1; + } + + /** + * Returns a location for the given {@link ClassNode}, where class node is + * either the top level class, or an inner class, in the current context. + * + * @param classNode the class in the current context + * @return a location pointing to the class declaration, or as close to it + * as possible + */ + @NonNull + public Location getLocation(@NonNull ClassNode classNode) { + // Attempt to find a proper location for this class. This is tricky + // since classes do not have line number entries in the class file; we need + // to find a method, look up the corresponding line number then search + // around it for a suitable tag, such as the class name. + String pattern; + if (isAnonymousClass(classNode.name)) { + pattern = classNode.superName; + } else { + pattern = classNode.name; + } + int index = pattern.lastIndexOf('$'); + if (index != -1) { + pattern = pattern.substring(index + 1); + } + index = pattern.lastIndexOf('/'); + if (index != -1) { + pattern = pattern.substring(index + 1); + } + + return getLocationForLine(findLineNumber(classNode), pattern, null, + SearchHints.create(BACKWARD).matchJavaSymbol()); + } + + @Nullable + private static MethodNode getFirstRealMethod(@NonNull ClassNode classNode) { + // Return the first method in the class for line number purposes. Skip , + // since it's typically not located near the real source of the method. + if (classNode.methods != null) { + @SuppressWarnings("rawtypes") // ASM API + List methods = classNode.methods; + for (Object m : methods) { + MethodNode method = (MethodNode) m; + if (method.name.charAt(0) != '<') { + return method; + } + } + + if (!classNode.methods.isEmpty()) { + return (MethodNode) classNode.methods.get(0); + } + } + + return null; + } + + /** + * Returns a location for the given {@link MethodNode}. + * + * @param methodNode the class in the current context + * @param classNode the class containing the method + * @return a location pointing to the class declaration, or as close to it + * as possible + */ + @NonNull + public Location getLocation(@NonNull MethodNode methodNode, + @NonNull ClassNode classNode) { + // Attempt to find a proper location for this class. This is tricky + // since classes do not have line number entries in the class file; we need + // to find a method, look up the corresponding line number then search + // around it for a suitable tag, such as the class name. + String pattern; + SearchDirection searchMode; + if (methodNode.name.equals(CONSTRUCTOR_NAME)) { + searchMode = EOL_BACKWARD; + if (isAnonymousClass(classNode.name)) { + pattern = classNode.superName.substring(classNode.superName.lastIndexOf('/') + 1); + } else { + pattern = classNode.name.substring(classNode.name.lastIndexOf('$') + 1); + } + } else { + searchMode = BACKWARD; + pattern = methodNode.name; + } + + return getLocationForLine(findLineNumber(methodNode), pattern, null, + SearchHints.create(searchMode).matchJavaSymbol()); + } + + /** + * Returns a location for the given {@link AbstractInsnNode}. + * + * @param instruction the instruction to look up the location for + * @return a location pointing to the instruction, or as close to it + * as possible + */ + @NonNull + public Location getLocation(@NonNull AbstractInsnNode instruction) { + SearchHints hints = SearchHints.create(FORWARD).matchJavaSymbol(); + String pattern = null; + if (instruction instanceof MethodInsnNode) { + MethodInsnNode call = (MethodInsnNode) instruction; + if (call.name.equals(CONSTRUCTOR_NAME)) { + pattern = call.owner; + hints = hints.matchConstructor(); + } else { + pattern = call.name; + } + int index = pattern.lastIndexOf('$'); + if (index != -1) { + pattern = pattern.substring(index + 1); + } + index = pattern.lastIndexOf('/'); + if (index != -1) { + pattern = pattern.substring(index + 1); + } + } + + int line = findLineNumber(instruction); + return getLocationForLine(line, pattern, null, hints); + } + + private static boolean isAnonymousClass(@NonNull String fqcn) { + int lastIndex = fqcn.lastIndexOf('$'); + if (lastIndex != -1 && lastIndex < fqcn.length() - 1) { + if (Character.isDigit(fqcn.charAt(lastIndex + 1))) { + return true; + } + } + return false; + } + + /** + * Converts from a VM owner name (such as foo/bar/Foo$Baz) to a + * fully qualified class name (such as foo.bar.Foo.Baz). + * + * @param owner the owner name to convert + * @return the corresponding fully qualified class name + */ + @NonNull + public static String getFqcn(@NonNull String owner) { + return owner.replace('/', '.').replace('$','.'); + } + + /** + * Computes a user-readable type signature from the given class owner, name + * and description. For example, for owner="foo/bar/Foo$Baz", name="foo", + * description="(I)V", it returns "void foo.bar.Foo.Bar#foo(int)". + * + * @param owner the class name + * @param name the method name + * @param desc the method description + * @return a user-readable string + */ + public static String createSignature(String owner, String name, String desc) { + StringBuilder sb = new StringBuilder(100); + + if (desc != null) { + Type returnType = Type.getReturnType(desc); + sb.append(getTypeString(returnType)); + sb.append(' '); + } + + if (owner != null) { + sb.append(getFqcn(owner)); + } + if (name != null) { + sb.append('#'); + sb.append(name); + if (desc != null) { + Type[] argumentTypes = Type.getArgumentTypes(desc); + if (argumentTypes != null && argumentTypes.length > 0) { + sb.append('('); + boolean first = true; + for (Type type : argumentTypes) { + if (first) { + first = false; + } else { + sb.append(", "); + } + sb.append(getTypeString(type)); + } + sb.append(')'); + } + } + } + + return sb.toString(); + } + + private static String getTypeString(Type type) { + String s = type.getClassName(); + if (s.startsWith("java.lang.")) { //$NON-NLS-1$ + s = s.substring("java.lang.".length()); //$NON-NLS-1$ + } + + return s; + } + + /** + * Computes the internal class name of the given fully qualified class name. + * For example, it converts foo.bar.Foo.Bar into foo/bar/Foo$Bar + * + * @param fqcn the fully qualified class name + * @return the internal class name + */ + @NonNull + public static String getInternalName(@NonNull String fqcn) { + if (fqcn.indexOf('.') == -1) { + return fqcn; + } + + // If class name contains $, it's not an ambiguous inner class name. + if (fqcn.indexOf('$') != -1) { + return fqcn.replace('.', '/'); + } + // Let's assume that components that start with Caps are class names. + StringBuilder sb = new StringBuilder(fqcn.length()); + String prev = null; + for (String part : Splitter.on('.').split(fqcn)) { + if (prev != null && !prev.isEmpty()) { + if (Character.isUpperCase(prev.charAt(0))) { + sb.append('$'); + } else { + sb.append('/'); + } + } + sb.append(part); + prev = part; + } + + return sb.toString(); + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Context.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Context.java new file mode 100644 index 00000000000..3a04d71d05c --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Context.java @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2011 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.detector.api; + +import static com.android.SdkConstants.DOT_GRADLE; +import static com.android.SdkConstants.DOT_JAVA; +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.SUPPRESS_ALL; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.lint.client.api.Configuration; +import com.android.tools.lint.client.api.LintClient; +import com.android.tools.lint.client.api.LintDriver; +import com.android.tools.lint.client.api.SdkInfo; +import com.google.common.annotations.Beta; + +import java.io.File; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +/** + * Context passed to the detectors during an analysis run. It provides + * information about the file being analyzed, it allows shared properties (so + * the detectors can share results), etc. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public class Context { + /** + * The file being checked. Note that this may not always be to a concrete + * file. For example, in the {@link Detector#beforeCheckProject(Context)} + * method, the context file is the directory of the project. + */ + public final File file; + + /** The driver running through the checks */ + protected final LintDriver mDriver; + + /** The project containing the file being checked */ + @NonNull + private final Project mProject; + + /** + * The "main" project. For normal projects, this is the same as {@link #mProject}, + * but for library projects, it's the root project that includes (possibly indirectly) + * the various library projects and their library projects. + *

+ * Note that this is a property on the {@link Context}, not the + * {@link Project}, since a library project can be included from multiple + * different top level projects, so there isn't one main project, + * just one per main project being analyzed with its library projects. + */ + private final Project mMainProject; + + /** The current configuration controlling which checks are enabled etc */ + private final Configuration mConfiguration; + + /** The contents of the file */ + private String mContents; + + /** Map of properties to share results between detectors */ + private Map mProperties; + + /** Whether this file contains any suppress markers (null means not yet determined) */ + private Boolean mContainsCommentSuppress; + + /** + * Construct a new {@link Context} + * + * @param driver the driver running through the checks + * @param project the project containing the file being checked + * @param main the main project if this project is a library project, or + * null if this is not a library project. The main project is + * the root project of all library projects, not necessarily the + * directly including project. + * @param file the file being checked + */ + public Context( + @NonNull LintDriver driver, + @NonNull Project project, + @Nullable Project main, + @NonNull File file) { + this.file = file; + + mDriver = driver; + mProject = project; + mMainProject = main; + mConfiguration = project.getConfiguration(); + } + + /** + * Returns the scope for the lint job + * + * @return the scope, never null + */ + @NonNull + public EnumSet getScope() { + return mDriver.getScope(); + } + + /** + * Returns the configuration for this project. + * + * @return the configuration, never null + */ + @NonNull + public Configuration getConfiguration() { + return mConfiguration; + } + + /** + * Returns the project containing the file being checked + * + * @return the project, never null + */ + @NonNull + public Project getProject() { + return mProject; + } + + /** + * Returns the main project if this project is a library project, or self + * if this is not a library project. The main project is the root project + * of all library projects, not necessarily the directly including project. + * + * @return the main project, never null + */ + @NonNull + public Project getMainProject() { + return mMainProject != null ? mMainProject : mProject; + } + + /** + * Returns the lint client requesting the lint check + * + * @return the client, never null + */ + @NonNull + public LintClient getClient() { + return mDriver.getClient(); + } + + /** + * Returns the driver running through the lint checks + * + * @return the driver + */ + @NonNull + public LintDriver getDriver() { + return mDriver; + } + + /** + * Returns the contents of the file. This may not be the contents of the + * file on disk, since it delegates to the {@link LintClient}, which in turn + * may decide to return the current edited contents of the file open in an + * editor. + * + * @return the contents of the given file, or null if an error occurs. + */ + @Nullable + public String getContents() { + if (mContents == null) { + mContents = mDriver.getClient().readFile(file); + } + + return mContents; + } + + /** + * Returns the value of the given named property, or null. + * + * @param name the name of the property + * @return the corresponding value, or null + */ + @SuppressWarnings("UnusedDeclaration") // Used in ADT + @Nullable + public Object getProperty(String name) { + if (mProperties == null) { + return null; + } + + return mProperties.get(name); + } + + /** + * Sets the value of the given named property. + * + * @param name the name of the property + * @param value the corresponding value + */ + @SuppressWarnings("UnusedDeclaration") // Used in ADT + public void setProperty(@NonNull String name, @Nullable Object value) { + if (value == null) { + if (mProperties != null) { + mProperties.remove(name); + } + } else { + if (mProperties == null) { + mProperties = new HashMap(); + } + mProperties.put(name, value); + } + } + + /** + * Gets the SDK info for the current project. + * + * @return the SDK info for the current project, never null + */ + @NonNull + public SdkInfo getSdkInfo() { + return mProject.getSdkInfo(); + } + + // ---- Convenience wrappers ---- (makes the detector code a bit leaner) + + /** + * Returns false if the given issue has been disabled. Convenience wrapper + * around {@link Configuration#getSeverity(Issue)}. + * + * @param issue the issue to check + * @return false if the issue has been disabled + */ + public boolean isEnabled(@NonNull Issue issue) { + return mConfiguration.isEnabled(issue); + } + + /** + * Reports an issue. Convenience wrapper around {@link LintClient#report} + * + * @param issue the issue to report + * @param location the location of the issue, or null if not known + * @param message the message for this warning + */ + public void report( + @NonNull Issue issue, + @Nullable Location location, + @NonNull String message) { + Configuration configuration = mConfiguration; + + // If this error was computed for a context where the context corresponds to + // a project instead of a file, the actual error may be in a different project (e.g. + // a library project), so adjust the configuration as necessary. + if (location != null && location.getFile() != null) { + Project project = mDriver.findProjectFor(location.getFile()); + if (project != null) { + configuration = project.getConfiguration(); + } + } + + // If an error occurs in a library project, but you've disabled that check in the + // main project, disable it in the library project too. (In some cases you don't + // control the lint.xml of a library project, and besides, if you're not interested in + // a check for your main project you probably don't care about it in the library either.) + if (configuration != mConfiguration + && mConfiguration.getSeverity(issue) == Severity.IGNORE) { + return; + } + + Severity severity = configuration.getSeverity(issue); + if (severity == Severity.IGNORE) { + return; + } + + mDriver.getClient().report(this, issue, severity, location, message, TextFormat.RAW); + } + + /** + * Report an error. + * Like {@link #report(Issue, Location, String)} but with + * a now-unused data parameter at the end + * + * @deprecated Use {@link #report(Issue, Location, String)} instead; + * this method is here for custom rule compatibility + */ + @SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules + @Deprecated + public void report( + @NonNull Issue issue, + @Nullable Location location, + @NonNull String message, + @SuppressWarnings("UnusedParameters") @Nullable Object data) { + report(issue, location, message); + } + + /** + * Send an exception to the log. Convenience wrapper around {@link LintClient#log}. + * + * @param exception the exception, possibly null + * @param format the error message using {@link String#format} syntax, possibly null + * @param args any arguments for the format string + */ + public void log( + @Nullable Throwable exception, + @Nullable String format, + @Nullable Object... args) { + mDriver.getClient().log(exception, format, args); + } + + /** + * Returns the current phase number. The first pass is numbered 1. Only one pass + * will be performed, unless a {@link Detector} calls {@link #requestRepeat}. + * + * @return the current phase, usually 1 + */ + public int getPhase() { + return mDriver.getPhase(); + } + + /** + * Requests another pass through the data for the given detector. This is + * typically done when a detector needs to do more expensive computation, + * but it only wants to do this once it knows that an error is + * present, or once it knows more specifically what to check for. + * + * @param detector the detector that should be included in the next pass. + * Note that the lint runner may refuse to run more than a couple + * of runs. + * @param scope the scope to be revisited. This must be a subset of the + * current scope ({@link #getScope()}, and it is just a performance hint; + * in particular, the detector should be prepared to be called on other + * scopes as well (since they may have been requested by other detectors). + * You can pall null to indicate "all". + */ + public void requestRepeat(@NonNull Detector detector, @Nullable EnumSet scope) { + mDriver.requestRepeat(detector, scope); + } + + /** Returns the comment marker used in Studio to suppress statements for language, if any */ + @Nullable + protected String getSuppressCommentPrefix() { + // Java and XML files are handled in sub classes (XmlContext, JavaContext) + + String path = file.getPath(); + if (path.endsWith(DOT_JAVA) || path.endsWith(DOT_GRADLE)) { + return JavaContext.SUPPRESS_COMMENT_PREFIX; + } else if (path.endsWith(DOT_XML)) { + return XmlContext.SUPPRESS_COMMENT_PREFIX; + } else if (path.endsWith(".cfg") || path.endsWith(".pro")) { + return "#suppress "; + } + + return null; + } + + /** Returns whether this file contains any suppress comment markers */ + public boolean containsCommentSuppress() { + if (mContainsCommentSuppress == null) { + mContainsCommentSuppress = false; + String prefix = getSuppressCommentPrefix(); + if (prefix != null) { + String contents = getContents(); + if (contents != null) { + mContainsCommentSuppress = contents.contains(prefix); + } + } + } + + return mContainsCommentSuppress; + } + + /** + * Returns true if the given issue is suppressed at the given character offset + * in the file's contents + */ + public boolean isSuppressedWithComment(int startOffset, @NonNull Issue issue) { + String prefix = getSuppressCommentPrefix(); + if (prefix == null) { + return false; + } + + if (startOffset <= 0) { + return false; + } + + // Check whether there is a comment marker + String contents = getContents(); + assert contents != null; // otherwise we wouldn't be here + if (startOffset >= contents.length()) { + return false; + } + + // Scan backwards to the previous line and see if it contains the marker + int lineStart = contents.lastIndexOf('\n', startOffset) + 1; + if (lineStart <= 1) { + return false; + } + int index = findPrefixOnPreviousLine(contents, lineStart, prefix); + if (index != -1 &&index+prefix.length() < lineStart) { + String line = contents.substring(index + prefix.length(), lineStart); + return line.contains(issue.getId()) + || line.contains(SUPPRESS_ALL) && line.trim().startsWith(SUPPRESS_ALL); + } + + return false; + } + + private static int findPrefixOnPreviousLine(String contents, int lineStart, String prefix) { + // Search backwards on the previous line until you find the prefix start (also look + // back on previous lines if the previous line(s) contain just whitespace + char first = prefix.charAt(0); + int offset = lineStart - 2; // 0: first char on this line, -1: \n on previous line, -2 last + boolean seenNonWhitespace = false; + for (; offset >= 0; offset--) { + char c = contents.charAt(offset); + if (seenNonWhitespace && c == '\n') { + return -1; + } + + if (!seenNonWhitespace && !Character.isWhitespace(c)) { + seenNonWhitespace = true; + } + + if (c == first && contents.regionMatches(false, offset, prefix, 0, + prefix.length())) { + return offset; + } + } + + return -1; + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/DefaultPosition.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/DefaultPosition.java new file mode 100644 index 00000000000..72c8ee7bc28 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/DefaultPosition.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2011 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.detector.api; + +import com.google.common.annotations.Beta; + +/** + * A simple offset-based position * + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public class DefaultPosition extends Position { + /** The line number (0-based where the first line is line 0) */ + private final int mLine; + + /** + * The column number (where the first character on the line is 0), or -1 if + * unknown + */ + private final int mColumn; + + /** The character offset */ + private final int mOffset; + + /** + * Creates a new {@link DefaultPosition} + * + * @param line the 0-based line number, or -1 if unknown + * @param column the 0-based column number, or -1 if unknown + * @param offset the offset, or -1 if unknown + */ + public DefaultPosition(int line, int column, int offset) { + mLine = line; + mColumn = column; + mOffset = offset; + } + + @Override + public int getLine() { + return mLine; + } + + @Override + public int getOffset() { + return mOffset; + } + + @Override + public int getColumn() { + return mColumn; + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Detector.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Detector.java new file mode 100644 index 00000000000..04e1937ee47 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Detector.java @@ -0,0 +1,778 @@ +/* + * Copyright (C) 2011 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.detector.api; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.resources.ResourceFolderType; +import com.android.tools.lint.client.api.JavaParser.ResolvedClass; +import com.android.tools.lint.client.api.JavaParser.ResolvedMethod; +import com.android.tools.lint.client.api.LintDriver; +import com.google.common.annotations.Beta; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +import lombok.ast.AstVisitor; +import lombok.ast.ClassDeclaration; +import lombok.ast.ConstructorInvocation; +import lombok.ast.MethodInvocation; +import lombok.ast.Node; + +/** + * A detector is able to find a particular problem (or a set of related problems). + * Each problem type is uniquely identified as an {@link Issue}. + *

+ * Detectors will be called in a predefined order: + *

    + *
  1. Manifest file + *
  2. Resource files, in alphabetical order by resource type + * (therefore, "layout" is checked before "values", "values-de" is checked before + * "values-en" but after "values", and so on. + *
  3. Java sources + *
  4. Java classes + *
  5. Gradle files + *
  6. Generic files + *
  7. Proguard files + *
  8. Property files + *
+ * If a detector needs information when processing a file type that comes from a type of + * file later in the order above, they can request a second phase; see + * {@link LintDriver#requestRepeat}. + * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public abstract class Detector { + /** Specialized interface for detectors that scan Java source file parse trees */ + public interface JavaScanner { + /** + * Create a parse tree visitor to process the parse tree. All + * {@link JavaScanner} detectors must provide a visitor, unless they + * either return true from {@link #appliesToResourceRefs()} or return + * non null from {@link #getApplicableMethodNames()}. + *

+ * If you return specific AST node types from + * {@link #getApplicableNodeTypes()}, then the visitor will only + * be called for the specific requested node types. This is more + * efficient, since it allows many detectors that apply to only a small + * part of the AST (such as method call nodes) to share iteration of the + * majority of the parse tree. + *

+ * If you return null from {@link #getApplicableNodeTypes()}, then your + * visitor will be called from the top and all node types visited. + *

+ * Note that a new visitor is created for each separate compilation + * unit, so you can store per file state in the visitor. + * + * @param context the {@link Context} for the file being analyzed + * @return a visitor, or null. + */ + @Nullable + AstVisitor createJavaVisitor(@NonNull JavaContext context); + + /** + * Return the types of AST nodes that the visitor returned from + * {@link #createJavaVisitor(JavaContext)} should visit. See the + * documentation for {@link #createJavaVisitor(JavaContext)} for details + * on how the shared visitor is used. + *

+ * If you return null from this method, then the visitor will process + * the full tree instead. + *

+ * Note that for the shared visitor, the return codes from the visit + * methods are ignored: returning true will not prune iteration + * of the subtree, since there may be other node types interested in the + * children. If you need to ensure that your visitor only processes a + * part of the tree, use a full visitor instead. See the + * OverdrawDetector implementation for an example of this. + * + * @return the list of applicable node types (AST node classes), or null + */ + @Nullable + List> getApplicableNodeTypes(); + + /** + * Return the list of method names this detector is interested in, or + * null. If this method returns non-null, then any AST nodes that match + * a method call in the list will be passed to the + * {@link #visitMethod(JavaContext, AstVisitor, MethodInvocation)} + * method for processing. The visitor created by + * {@link #createJavaVisitor(JavaContext)} is also passed to that + * method, although it can be null. + *

+ * This makes it easy to write detectors that focus on some fixed calls. + * For example, the StringFormatDetector uses this mechanism to look for + * "format" calls, and when found it looks around (using the AST's + * {@link Node#getParent()} method) to see if it's called on + * a String class instance, and if so do its normal processing. Note + * that since it doesn't need to do any other AST processing, that + * detector does not actually supply a visitor. + * + * @return a set of applicable method names, or null. + */ + @Nullable + List getApplicableMethodNames(); + + /** + * Method invoked for any method calls found that matches any names + * returned by {@link #getApplicableMethodNames()}. This also passes + * back the visitor that was created by + * {@link #createJavaVisitor(JavaContext)}, but a visitor is not + * required. It is intended for detectors that need to do additional AST + * processing, but also want the convenience of not having to look for + * method names on their own. + * + * @param context the context of the lint request + * @param visitor the visitor created from + * {@link #createJavaVisitor(JavaContext)}, or null + * @param node the {@link MethodInvocation} node for the invoked method + */ + void visitMethod( + @NonNull JavaContext context, + @Nullable AstVisitor visitor, + @NonNull MethodInvocation node); + + /** + * Return the list of constructor types this detector is interested in, or + * null. If this method returns non-null, then any AST nodes that match + * a constructor call in the list will be passed to the + * {@link #visitConstructor(JavaContext, AstVisitor, ConstructorInvocation, ResolvedMethod)} + * method for processing. The visitor created by + * {@link #createJavaVisitor(JavaContext)} is also passed to that + * method, although it can be null. + *

+ * This makes it easy to write detectors that focus on some fixed constructors. + * + * @return a set of applicable fully qualified types, or null. + */ + @Nullable + List getApplicableConstructorTypes(); + + /** + * Method invoked for any constructor calls found that matches any names + * returned by {@link #getApplicableConstructorTypes()}. This also passes + * back the visitor that was created by + * {@link #createJavaVisitor(JavaContext)}, but a visitor is not + * required. It is intended for detectors that need to do additional AST + * processing, but also want the convenience of not having to look for + * method names on their own. + * + * @param context the context of the lint request + * @param visitor the visitor created from + * {@link #createJavaVisitor(JavaContext)}, or null + * @param node the {@link ConstructorInvocation} node for the invoked method + * @param constructor the resolved constructor method with type information + */ + void visitConstructor( + @NonNull JavaContext context, + @Nullable AstVisitor visitor, + @NonNull ConstructorInvocation node, + @NonNull ResolvedMethod constructor); + + /** + * Returns whether this detector cares about Android resource references + * (such as {@code R.layout.main} or {@code R.string.app_name}). If it + * does, then the visitor will look for these patterns, and if found, it + * will invoke {@link #visitResourceReference} passing the resource type + * and resource name. It also passes the visitor, if any, that was + * created by {@link #createJavaVisitor(JavaContext)}, such that a + * detector can do more than just look for resources. + * + * @return true if this detector wants to be notified of R resource + * identifiers found in the code. + */ + boolean appliesToResourceRefs(); + + /** + * Called for any resource references (such as {@code R.layout.main} + * found in Java code, provided this detector returned {@code true} from + * {@link #appliesToResourceRefs()}. + * + * @param context the lint scanning context + * @param visitor the visitor created from + * {@link #createJavaVisitor(JavaContext)}, or null + * @param node the variable reference for the resource + * @param type the resource type, such as "layout" or "string" + * @param name the resource name, such as "main" from + * {@code R.layout.main} + * @param isFramework whether the resource is a framework resource + * (android.R) or a local project resource (R) + */ + void visitResourceReference( + @NonNull JavaContext context, + @Nullable AstVisitor visitor, + @NonNull Node node, + @NonNull String type, + @NonNull String name, + boolean isFramework); + + /** + * Returns a list of fully qualified names for super classes that this + * detector cares about. If not null, this detector will *only* be called + * if the current class is a subclass of one of the specified superclasses. + * + * @return a list of fully qualified names + */ + @Nullable + List applicableSuperClasses(); + + /** + * Called for each class that extends one of the super classes specified with + * {@link #applicableSuperClasses()} + * + * @param context the lint scanning context + * @param declaration the class declaration node, or null for anonymous classes + * @param node the class declaration node or the anonymous class construction node + * @param resolvedClass the resolved class + */ + void checkClass(@NonNull JavaContext context, @Nullable ClassDeclaration declaration, + @NonNull Node node, @NonNull ResolvedClass resolvedClass); + } + + /** Specialized interface for detectors that scan Java class files */ + public interface ClassScanner { + /** + * Checks the given class' bytecode for issues. + * + * @param context the context of the lint check, pointing to for example + * the file + * @param classNode the root class node + */ + void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode); + + /** + * Returns the list of node types (corresponding to the constants in the + * {@link AbstractInsnNode} class) that this scanner applies to. The + * {@link #checkInstruction(ClassContext, ClassNode, MethodNode, AbstractInsnNode)} + * method will be called for each match. + * + * @return an array containing all the node types this detector should be + * called for, or null if none. + */ + @Nullable + int[] getApplicableAsmNodeTypes(); + + /** + * Process a given instruction node, and register lint issues if + * applicable. + * + * @param context the context of the lint check, pointing to for example + * the file + * @param classNode the root class node + * @param method the method node containing the call + * @param instruction the actual instruction + */ + void checkInstruction(@NonNull ClassContext context, @NonNull ClassNode classNode, + @NonNull MethodNode method, @NonNull AbstractInsnNode instruction); + + /** + * Return the list of method call names (in VM format, e.g. "" for + * constructors, etc) for method calls this detector is interested in, + * or null. T his will be used to dispatch calls to + * {@link #checkCall(ClassContext, ClassNode, MethodNode, MethodInsnNode)} + * for only the method calls in owners that the detector is interested + * in. + *

+ * NOTE: If you return non null from this method, then only + * {@link #checkCall(ClassContext, ClassNode, MethodNode, MethodInsnNode)} + * will be called if a suitable method is found; + * {@link #checkClass(ClassContext, ClassNode)} will not be called under + * any circumstances. + *

+ * This makes it easy to write detectors that focus on some fixed calls, + * and allows lint to make a single pass over the bytecode over a class, + * and efficiently dispatch method calls to any detectors that are + * interested in it. Without this, each new lint check interested in a + * single method, would be doing a complete pass through all the + * bytecode instructions of the class via the + * {@link #checkClass(ClassContext, ClassNode)} method, which would make + * each newly added lint check make lint slower. Now a single dispatch + * map is used instead, and for each encountered call in the single + * dispatch, it looks up in the map which if any detectors are + * interested in the given call name, and dispatches to each one in + * turn. + * + * @return a list of applicable method names, or null. + */ + @Nullable + List getApplicableCallNames(); + + /** + * Just like {@link Detector#getApplicableCallNames()}, but for the owner + * field instead. The + * {@link #checkCall(ClassContext, ClassNode, MethodNode, MethodInsnNode)} + * method will be called for all {@link MethodInsnNode} instances where the + * owner field matches any of the members returned in this node. + *

+ * Note that if your detector provides both a name and an owner, the + * method will be called for any nodes matching either the name or + * the owner, not only where they match both. Note also that it will + * be called twice - once for the name match, and (at least once) for the owner + * match. + * + * @return a list of applicable owner names, or null. + */ + @Nullable + List getApplicableCallOwners(); + + /** + * Process a given method call node, and register lint issues if + * applicable. This is similar to the + * {@link #checkInstruction(ClassContext, ClassNode, MethodNode, AbstractInsnNode)} + * method, but has the additional advantage that it is only called for known + * method names or method owners, according to + * {@link #getApplicableCallNames()} and {@link #getApplicableCallOwners()}. + * + * @param context the context of the lint check, pointing to for example + * the file + * @param classNode the root class node + * @param method the method node containing the call + * @param call the actual method call node + */ + void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, + @NonNull MethodNode method, @NonNull MethodInsnNode call); + } + + /** Specialized interface for detectors that scan binary resource files */ + public interface BinaryResourceScanner { + /** + * Called for each resource folder + * + * @param context the context for the resource file + */ + void checkBinaryResource(@NonNull ResourceContext context); + + /** + * Returns whether this detector applies to the given folder type. This + * allows the detectors to be pruned from iteration, so for example when we + * are analyzing a string value file we don't need to look up detectors + * related to layout. + * + * @param folderType the folder type to be visited + * @return true if this detector can apply to resources in folders of the + * given type + */ + boolean appliesTo(@NonNull ResourceFolderType folderType); + } + + /** Specialized interface for detectors that scan resource folders (the folder directory + * itself, not the individual files within it */ + public interface ResourceFolderScanner { + /** + * Called for each resource folder + * + * @param context the context for the resource folder + * @param folderName the resource folder name + */ + void checkFolder(@NonNull ResourceContext context, @NonNull String folderName); + + /** + * Returns whether this detector applies to the given folder type. This + * allows the detectors to be pruned from iteration, so for example when we + * are analyzing a string value file we don't need to look up detectors + * related to layout. + * + * @param folderType the folder type to be visited + * @return true if this detector can apply to resources in folders of the + * given type + */ + boolean appliesTo(@NonNull ResourceFolderType folderType); + } + + /** Specialized interface for detectors that scan XML files */ + public interface XmlScanner { + /** + * Visit the given document. The detector is responsible for its own iteration + * through the document. + * @param context information about the document being analyzed + * @param document the document to examine + */ + void visitDocument(@NonNull XmlContext context, @NonNull Document document); + + /** + * Visit the given element. + * @param context information about the document being analyzed + * @param element the element to examine + */ + void visitElement(@NonNull XmlContext context, @NonNull Element element); + + /** + * Visit the given element after its children have been analyzed. + * @param context information about the document being analyzed + * @param element the element to examine + */ + void visitElementAfter(@NonNull XmlContext context, @NonNull Element element); + + /** + * Visit the given attribute. + * @param context information about the document being analyzed + * @param attribute the attribute node to examine + */ + void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute); + + /** + * Returns the list of elements that this detector wants to analyze. If non + * null, this detector will be called (specifically, the + * {@link #visitElement} method) for each matching element in the document. + *

+ * If this method returns null, and {@link #getApplicableAttributes()} also returns + * null, then the {@link #visitDocument} method will be called instead. + * + * @return a collection of elements, or null, or the special + * {@link XmlScanner#ALL} marker to indicate that every single + * element should be analyzed. + */ + @Nullable + Collection getApplicableElements(); + + /** + * Returns the list of attributes that this detector wants to analyze. If non + * null, this detector will be called (specifically, the + * {@link #visitAttribute} method) for each matching attribute in the document. + *

+ * If this method returns null, and {@link #getApplicableElements()} also returns + * null, then the {@link #visitDocument} method will be called instead. + * + * @return a collection of attributes, or null, or the special + * {@link XmlScanner#ALL} marker to indicate that every single + * attribute should be analyzed. + */ + @Nullable + Collection getApplicableAttributes(); + + /** + * Special marker collection returned by {@link #getApplicableElements()} or + * {@link #getApplicableAttributes()} to indicate that the check should be + * invoked on all elements or all attributes + */ + @NonNull + List ALL = new ArrayList(0); // NOT Collections.EMPTY! + // We want to distinguish this from just an *empty* list returned by the caller! + } + + /** Specialized interface for detectors that scan Gradle files */ + public interface GradleScanner { + void visitBuildScript(@NonNull Context context, Map sharedData); + } + + /** Specialized interface for detectors that scan other files */ + public interface OtherFileScanner { + /** + * Returns the set of files this scanner wants to consider. If this includes + * {@link Scope#OTHER} then all source files will be checked. Note that the + * set of files will not just include files of the indicated type, but all files + * within the relevant source folder. For example, returning {@link Scope#JAVA_FILE} + * will not just return {@code .java} files, but also other resource files such as + * {@code .html} and other files found within the Java source folders. + *

+ * Lint will call the {@link #run(Context)}} method when the file should be checked. + * + * @return set of scopes that define the types of source files the + * detector wants to consider + */ + @NonNull + EnumSet getApplicableFiles(); + } + + /** + * Runs the detector. This method will not be called for certain specialized + * detectors, such as {@link XmlScanner} and {@link JavaScanner}, where + * there are specialized analysis methods instead such as + * {@link XmlScanner#visitElement(XmlContext, Element)}. + * + * @param context the context describing the work to be done + */ + public void run(@NonNull Context context) { + } + + /** + * Returns true if this detector applies to the given file + * + * @param context the context to check + * @param file the file in the context to check + * @return true if this detector applies to the given context and file + */ + public boolean appliesTo(@NonNull Context context, @NonNull File file) { + return false; + } + + /** + * Analysis is about to begin, perform any setup steps. + * + * @param context the context for the check referencing the project, lint + * client, etc + */ + public void beforeCheckProject(@NonNull Context context) { + } + + /** + * Analysis has just been finished for the whole project, perform any + * cleanup or report issues that require project-wide analysis. + * + * @param context the context for the check referencing the project, lint + * client, etc + */ + public void afterCheckProject(@NonNull Context context) { + } + + /** + * Analysis is about to begin for the given library project, perform any setup steps. + * + * @param context the context for the check referencing the project, lint + * client, etc + */ + public void beforeCheckLibraryProject(@NonNull Context context) { + } + + /** + * Analysis has just been finished for the given library project, perform any + * cleanup or report issues that require library-project-wide analysis. + * + * @param context the context for the check referencing the project, lint + * client, etc + */ + public void afterCheckLibraryProject(@NonNull Context context) { + } + + /** + * Analysis is about to be performed on a specific file, perform any setup + * steps. + *

+ * Note: When this method is called at the beginning of checking an XML + * file, the context is guaranteed to be an instance of {@link XmlContext}, + * and similarly for a Java source file, the context will be a + * {@link JavaContext} and so on. + * + * @param context the context for the check referencing the file to be + * checked, the project, etc. + */ + public void beforeCheckFile(@NonNull Context context) { + } + + /** + * Analysis has just been finished for a specific file, perform any cleanup + * or report issues found + *

+ * Note: When this method is called at the end of checking an XML + * file, the context is guaranteed to be an instance of {@link XmlContext}, + * and similarly for a Java source file, the context will be a + * {@link JavaContext} and so on. + * + * @param context the context for the check referencing the file to be + * checked, the project, etc. + */ + public void afterCheckFile(@NonNull Context context) { + } + + /** + * Returns the expected speed of this detector + * + * @return the expected speed of this detector + */ + @NonNull + public Speed getSpeed() { + return Speed.NORMAL; + } + + /** + * Returns the expected speed of this detector. + * The issue parameter is made available for subclasses which analyze multiple issues + * and which need to distinguish implementation cost by issue. If the detector does + * not analyze multiple issues or does not vary in speed by issue type, just override + * {@link #getSpeed()} instead. + * + * @param issue the issue to look up the analysis speed for + * @return the expected speed of this detector + */ + @NonNull + public Speed getSpeed(@SuppressWarnings("UnusedParameters") @NonNull Issue issue) { + // If not overridden, this detector does not distinguish speed by issue type + return getSpeed(); + } + + // ---- Dummy implementations to make implementing XmlScanner easier: ---- + + @SuppressWarnings("javadoc") + public void visitDocument(@NonNull XmlContext context, @NonNull Document document) { + // This method must be overridden if your detector does + // not return something from getApplicableElements or + // getApplicableAttributes + assert false; + } + + @SuppressWarnings("javadoc") + public void visitElement(@NonNull XmlContext context, @NonNull Element element) { + // This method must be overridden if your detector returns + // tag names from getApplicableElements + assert false; + } + + @SuppressWarnings("javadoc") + public void visitElementAfter(@NonNull XmlContext context, @NonNull Element element) { + } + + @SuppressWarnings("javadoc") + public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { + // This method must be overridden if your detector returns + // attribute names from getApplicableAttributes + assert false; + } + + @SuppressWarnings("javadoc") + @Nullable + public Collection getApplicableElements() { + return null; + } + + @Nullable + @SuppressWarnings("javadoc") + public Collection getApplicableAttributes() { + return null; + } + + // ---- Dummy implementations to make implementing JavaScanner easier: ---- + + @Nullable @SuppressWarnings("javadoc") + public List getApplicableMethodNames() { + return null; + } + + @Nullable @SuppressWarnings("javadoc") + public AstVisitor createJavaVisitor(@NonNull JavaContext context) { + return null; + } + + @Nullable @SuppressWarnings("javadoc") + public List> getApplicableNodeTypes() { + return null; + } + + @SuppressWarnings("javadoc") + public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor, + @NonNull MethodInvocation node) { + } + + @SuppressWarnings("javadoc") + public boolean appliesToResourceRefs() { + return false; + } + + @SuppressWarnings("javadoc") + public void visitResourceReference(@NonNull JavaContext context, @Nullable AstVisitor visitor, + @NonNull Node node, @NonNull String type, @NonNull String name, + boolean isFramework) { + } + + @Nullable + public List applicableSuperClasses() { + return null; + } + + public void checkClass(@NonNull JavaContext context, @Nullable ClassDeclaration declaration, + @NonNull Node node, @NonNull ResolvedClass resolvedClass) { + } + + @Nullable @SuppressWarnings("javadoc") + public List getApplicableConstructorTypes() { + return null; + } + + @SuppressWarnings("javadoc") + public void visitConstructor( + @NonNull JavaContext context, + @Nullable AstVisitor visitor, + @NonNull ConstructorInvocation node, + @NonNull ResolvedMethod constructor) { + } + + // ---- Dummy implementations to make implementing a ClassScanner easier: ---- + + @SuppressWarnings("javadoc") + public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) { + } + + @SuppressWarnings("javadoc") + @Nullable + public List getApplicableCallNames() { + return null; + } + + @SuppressWarnings("javadoc") + @Nullable + public List getApplicableCallOwners() { + return null; + } + + @SuppressWarnings("javadoc") + public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, + @NonNull MethodNode method, @NonNull MethodInsnNode call) { + } + + @SuppressWarnings("javadoc") + @Nullable + public int[] getApplicableAsmNodeTypes() { + return null; + } + + @SuppressWarnings("javadoc") + public void checkInstruction(@NonNull ClassContext context, @NonNull ClassNode classNode, + @NonNull MethodNode method, @NonNull AbstractInsnNode instruction) { + } + + // ---- Dummy implementations to make implementing an OtherFileScanner easier: ---- + + public boolean appliesToFolder(@NonNull Scope scope, @Nullable ResourceFolderType folderType) { + return false; + } + + @NonNull + public EnumSet getApplicableFiles() { + return Scope.OTHER_SCOPE; + } + + // ---- Dummy implementations to make implementing an GradleScanner easier: ---- + + public void visitBuildScript(@NonNull Context context, Map sharedData) { + } + + // ---- Dummy implementations to make implementing a resource folder scanner easier: ---- + + public void checkFolder(@NonNull ResourceContext context, @NonNull String folderName) { + } + + // ---- Dummy implementations to make implementing a binary resource scanner easier: ---- + + public void checkBinaryResource(@NonNull ResourceContext context) { + } + + public boolean appliesTo(@NonNull ResourceFolderType folderType) { + return true; + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Implementation.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Implementation.java new file mode 100644 index 00000000000..9a4b70315a1 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Implementation.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2013 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.detector.api; + +import com.android.annotations.NonNull; +import com.google.common.annotations.Beta; + +import java.util.EnumSet; + +/** + * An {@linkplain Implementation} of an {@link Issue} maps to the {@link Detector} + * class responsible for analyzing the issue, as well as the {@link Scope} required + * by the detector to perform its analysis. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public class Implementation { + private final Class mClass; + private final EnumSet mScope; + private EnumSet[] mAnalysisScopes; + + @SuppressWarnings("unchecked") + private static final EnumSet[] EMPTY = new EnumSet[0]; + + /** + * Creates a new implementation for analyzing one or more issues + * + * @param detectorClass the class of the detector to find this issue + * @param scope the scope of files required to analyze this issue + */ + @SuppressWarnings("unchecked") + public Implementation( + @NonNull Class detectorClass, + @NonNull EnumSet scope) { + this(detectorClass, scope, EMPTY); + } + + /** + * Creates a new implementation for analyzing one or more issues + * + * @param detectorClass the class of the detector to find this issue + * @param scope the scope of files required to analyze this issue + * @param analysisScopes optional set of extra scopes the detector is capable of working in + */ + public Implementation( + @NonNull Class detectorClass, + @NonNull EnumSet scope, + @NonNull EnumSet... analysisScopes) { + mClass = detectorClass; + mScope = scope; + mAnalysisScopes = analysisScopes; + } + + /** + * Returns the class of the detector to use to find this issue + * + * @return the class of the detector to use to find this issue + */ + @NonNull + public Class getDetectorClass() { + return mClass; + } + + @Override + public String toString() { + return mClass.toString(); + } + + /** + * Returns the scope required to analyze the code to detect this issue. + * This is determined by the detectors which reports the issue. + * + * @return the required scope + */ + @NonNull + public EnumSet getScope() { + return mScope; + } + + /** + * Returns the sets of scopes required to analyze this issue, or null if all + * scopes named by {@link #getScope()} are necessary. Note that only + * one match out of this collection is required, not all, and that + * the scope set returned by {@link #getScope()} does not have to be returned + * by this method, but is always implied to be included. + *

+ * The scopes returned by {@link #getScope()} list all the various + * scopes that are affected by this issue, meaning the detector + * should consider it. Frequently, the detector must analyze all these + * scopes in order to properly decide whether an issue is found. For + * example, the unused resource detector needs to consider both the XML + * resource files and the Java source files in order to decide if a resource + * is unused. If it analyzes just the Java files for example, it might + * incorrectly conclude that a resource is unused because it did not + * discover a resource reference in an XML file. + *

+ * However, there are other issues where the issue can occur in a variety of + * files, but the detector can consider each in isolation. For example, the + * API checker is affected by both XML files and Java class files (detecting + * both layout constructor references in XML layout files as well as code + * references in .class files). It doesn't have to analyze both; it is + * capable of incrementally analyzing just an XML file, or just a class + * file, without considering the other. + *

+ * The required scope list provides a list of scope sets that can be used to + * analyze this issue. For each scope set, all the scopes must be matched by + * the incremental analysis, but any one of the scope sets can be analyzed + * in isolation. + *

+ * The required scope list is not required to include the full scope set + * returned by {@link #getScope()}; that set is always assumed to be + * included. + *

+ * NOTE: You would normally call {@link #isAdequate(EnumSet)} rather + * than calling this method directly. + * + * @return a list of required scopes, or null. + */ + @NonNull + public EnumSet[] getAnalysisScopes() { + return mAnalysisScopes; + } + + /** + * Returns true if the given scope is adequate for analyzing this issue. + * This looks through the analysis scopes (see + * {@link #getAnalysisScopes()}) and if the scope passed in fully + * covers at least one of them, or if it covers the scope of the issue + * itself (see {@link #getScope()}, which should be a superset of all the + * analysis scopes) returns true. + *

+ * The scope set returned by {@link #getScope()} lists all the various + * scopes that are affected by this issue, meaning the detector + * should consider it. Frequently, the detector must analyze all these + * scopes in order to properly decide whether an issue is found. For + * example, the unused resource detector needs to consider both the XML + * resource files and the Java source files in order to decide if a resource + * is unused. If it analyzes just the Java files for example, it might + * incorrectly conclude that a resource is unused because it did not + * discover a resource reference in an XML file. + *

+ * However, there are other issues where the issue can occur in a variety of + * files, but the detector can consider each in isolation. For example, the + * API checker is affected by both XML files and Java class files (detecting + * both layout constructor references in XML layout files as well as code + * references in .class files). It doesn't have to analyze both; it is + * capable of incrementally analyzing just an XML file, or just a class + * file, without considering the other. + *

+ * An issue can register additional scope sets that can are adequate + * for analyzing the issue, by supplying it to + * {@link #Implementation(Class, java.util.EnumSet, java.util.EnumSet[])}. + * This method returns true if the given scope matches one or more analysis + * scope, or the overall scope. + * + * @param scope the scope available for analysis + * @return true if this issue can be analyzed with the given available scope + */ + public boolean isAdequate(@NonNull EnumSet scope) { + if (scope.containsAll(mScope)) { + return true; + } + + if (mAnalysisScopes != null) { + for (EnumSet analysisScope : mAnalysisScopes) { + if (scope.containsAll(analysisScope)) { + return true; + } + } + } + + return false; + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Issue.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Issue.java new file mode 100644 index 00000000000..eb2f15e03a0 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Issue.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2011 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.detector.api; + +import static com.android.tools.lint.detector.api.TextFormat.RAW; + +import com.android.annotations.NonNull; +import com.android.tools.lint.client.api.Configuration; +import com.google.common.annotations.Beta; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +/** + * An issue is a potential bug in an Android application. An issue is discovered + * by a {@link Detector}, and has an associated {@link Severity}. + *

+ * Issues and detectors are separate classes because a detector can discover + * multiple different issues as it's analyzing code, and we want to be able to + * different severities for different issues, the ability to suppress one but + * not other issues from the same detector, and so on. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public final class Issue implements Comparable { + private final String mId; + private final String mBriefDescription; + private final String mExplanation; + private final Category mCategory; + private final int mPriority; + private final Severity mSeverity; + private Object mMoreInfoUrls; + private boolean mEnabledByDefault = true; + private Implementation mImplementation; + + // Use factory methods + private Issue( + @NonNull String id, + @NonNull String shortDescription, + @NonNull String explanation, + @NonNull Category category, + int priority, + @NonNull Severity severity, + @NonNull Implementation implementation) { + assert !shortDescription.isEmpty(); + assert !explanation.isEmpty(); + + mId = id; + mBriefDescription = shortDescription; + mExplanation = explanation; + mCategory = category; + mPriority = priority; + mSeverity = severity; + mImplementation = implementation; + } + + /** + * Creates a new issue. The description strings can use some simple markup; + * see the {@link TextFormat#RAW} documentation + * for details. + * + * @param id the fixed id of the issue + * @param briefDescription short summary (typically 5-6 words or less), typically + * describing the problem rather than the fix + * (e.g. "Missing minSdkVersion") + * @param explanation a full explanation of the issue, with suggestions for + * how to fix it + * @param category the associated category, if any + * @param priority the priority, a number from 1 to 10 with 10 being most + * important/severe + * @param severity the default severity of the issue + * @param implementation the default implementation for this issue + * @return a new {@link Issue} + */ + @NonNull + public static Issue create( + @NonNull String id, + @NonNull String briefDescription, + @NonNull String explanation, + @NonNull Category category, + int priority, + @NonNull Severity severity, + @NonNull Implementation implementation) { + return new Issue(id, briefDescription, explanation, category, priority, + severity, implementation); + } + + /** + * For compatibility with older custom rules) + * + * @deprecated Use {@link #create(String, String, String, Category, int, Severity, Implementation)} instead + */ + @NonNull + @Deprecated + public static Issue create( + @NonNull String id, + @NonNull String briefDescription, + @SuppressWarnings("UnusedParameters") @NonNull String description, + @NonNull String explanation, + @NonNull Category category, + int priority, + @NonNull Severity severity, + @NonNull Implementation implementation) { + return new Issue(id, briefDescription, explanation, category, priority, + severity, implementation); + } + + /** + * Returns the unique id of this issue. These should not change over time + * since they are used to persist the names of issues suppressed by the user + * etc. It is typically a single camel-cased word. + * + * @return the associated fixed id, never null and always unique + */ + @NonNull + public String getId() { + return mId; + } + + /** + * Briefly (in a couple of words) describes these errors + * + * @return a brief summary of the issue, never null, never empty + */ + @NonNull + public String getBriefDescription(@NonNull TextFormat format) { + return RAW.convertTo(mBriefDescription, format); + } + + /** + * Describes the error found by this rule, e.g. + * "Buttons must define contentDescriptions". Preferably the explanation + * should also contain a description of how the problem should be solved. + * Additional info can be provided via {@link #getMoreInfo()}. + * + * @param format the format to write the format as + * @return an explanation of the issue, never null, never empty + */ + @NonNull + public String getExplanation(@NonNull TextFormat format) { + return RAW.convertTo(mExplanation, format); + } + + /** + * The primary category of the issue + * + * @return the primary category of the issue, never null + */ + @NonNull + public Category getCategory() { + return mCategory; + } + + /** + * Returns a priority, in the range 1-10, with 10 being the most severe and + * 1 the least + * + * @return a priority from 1 to 10 + */ + public int getPriority() { + return mPriority; + } + + /** + * Returns the default severity of the issues found by this detector (some + * tools may allow the user to specify custom severities for detectors). + *

+ * Note that even though the normal way for an issue to be disabled is for + * the {@link Configuration} to return {@link Severity#IGNORE}, there is a + * {@link #isEnabledByDefault()} method which can be used to turn off issues + * by default. This is done rather than just having the severity as the only + * attribute on the issue such that an issue can be configured with an + * appropriate severity (such as {@link Severity#ERROR}) even when issues + * are disabled by default for example because they are experimental or not + * yet stable. + * + * @return the severity of the issues found by this detector + */ + @NonNull + public Severity getDefaultSeverity() { + return mSeverity; + } + + /** + * Returns a link (a URL string) to more information, or null + * + * @return a link to more information, or null + */ + @NonNull + public List getMoreInfo() { + if (mMoreInfoUrls == null) { + return Collections.emptyList(); + } else if (mMoreInfoUrls instanceof String) { + return Collections.singletonList((String) mMoreInfoUrls); + } else { + assert mMoreInfoUrls instanceof List; + //noinspection unchecked + return (List) mMoreInfoUrls; + } + } + + /** + * Adds a more info URL string + * + * @param moreInfoUrl url string + * @return this, for constructor chaining + */ + @NonNull + public Issue addMoreInfo(@NonNull String moreInfoUrl) { + // Nearly all issues supply at most a single URL, so don't bother with + // lists wrappers for most of these issues + if (mMoreInfoUrls == null) { + mMoreInfoUrls = moreInfoUrl; + } else if (mMoreInfoUrls instanceof String) { + String existing = (String) mMoreInfoUrls; + List list = new ArrayList(2); + list.add(existing); + list.add(moreInfoUrl); + mMoreInfoUrls = list; + } else { + assert mMoreInfoUrls instanceof List; + //noinspection unchecked + ((List) mMoreInfoUrls).add(moreInfoUrl); + } + return this; + } + + /** + * Returns whether this issue should be enabled by default, unless the user + * has explicitly disabled it. + * + * @return true if this issue should be enabled by default + */ + public boolean isEnabledByDefault() { + return mEnabledByDefault; + } + + /** + * Returns the implementation for the given issue + * + * @return the implementation for this issue + */ + @NonNull + public Implementation getImplementation() { + return mImplementation; + } + + /** + * Sets the implementation for the given issue. This is typically done by + * IDEs that can offer a replacement for a given issue which performs better + * or in some other way works better within the IDE. + * + * @param implementation the new implementation to use + */ + public void setImplementation(@NonNull Implementation implementation) { + mImplementation = implementation; + } + + /** + * Sorts the detectors alphabetically by id. This is intended to make it + * convenient to store settings for detectors in a fixed order. It is not + * intended as the order to be shown to the user; for that, a tool embedding + * lint might consider the priorities, categories, severities etc of the + * various detectors. + * + * @param other the {@link Issue} to compare this issue to + */ + @Override + public int compareTo(@NonNull Issue other) { + return getId().compareTo(other.getId()); + } + + /** + * Sets whether this issue is enabled by default. + * + * @param enabledByDefault whether the issue should be enabled by default + * @return this, for constructor chaining + */ + @NonNull + public Issue setEnabledByDefault(boolean enabledByDefault) { + mEnabledByDefault = enabledByDefault; + return this; + } + + @Override + public String toString() { + return mId; + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/JavaContext.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/JavaContext.java new file mode 100644 index 00000000000..5000c26e67c --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/JavaContext.java @@ -0,0 +1,394 @@ +/* + * Copyright (C) 2011 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.detector.api; + +import static com.android.SdkConstants.CLASS_CONTEXT; +import static com.android.tools.lint.client.api.JavaParser.ResolvedNode; +import static com.android.tools.lint.client.api.JavaParser.TypeDescriptor; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.tools.lint.client.api.JavaParser; +import com.android.tools.lint.client.api.JavaParser.ResolvedClass; +import com.android.tools.lint.client.api.LintDriver; + +import java.io.File; +import java.util.Iterator; + +import lombok.ast.ClassDeclaration; +import lombok.ast.ConstructorDeclaration; +import lombok.ast.Expression; +import lombok.ast.MethodDeclaration; +import lombok.ast.MethodInvocation; +import lombok.ast.Node; +import lombok.ast.Position; + +/** + * A {@link Context} used when checking Java files. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +public class JavaContext extends Context { + static final String SUPPRESS_COMMENT_PREFIX = "//noinspection "; //$NON-NLS-1$ + + /** The parse tree */ + private Node mCompilationUnit; + + /** The parser which produced the parse tree */ + private final JavaParser mParser; + + /** + * Constructs a {@link JavaContext} for running lint on the given file, with + * the given scope, in the given project reporting errors to the given + * client. + * + * @param driver the driver running through the checks + * @param project the project to run lint on which contains the given file + * @param main the main project if this project is a library project, or + * null if this is not a library project. The main project is + * the root project of all library projects, not necessarily the + * directly including project. + * @param file the file to be analyzed + * @param parser the parser to use + */ + public JavaContext( + @NonNull LintDriver driver, + @NonNull Project project, + @Nullable Project main, + @NonNull File file, + @NonNull JavaParser parser) { + super(driver, project, main, file); + mParser = parser; + } + + /** + * Returns a location for the given node + * + * @param node the AST node to get a location for + * @return a location for the given node + */ + @NonNull + public Location getLocation(@NonNull Node node) { + return mParser.getLocation(this, node); + } + + @NonNull + public JavaParser getParser() { + return mParser; + } + + @Nullable + public Node getCompilationUnit() { + return mCompilationUnit; + } + + /** + * Sets the compilation result. Not intended for client usage; the lint infrastructure + * will set this when a context has been processed + * + * @param compilationUnit the parse tree + */ + public void setCompilationUnit(@Nullable Node compilationUnit) { + mCompilationUnit = compilationUnit; + } + + @Override + public void report(@NonNull Issue issue, @Nullable Location location, + @NonNull String message) { + if (mDriver.isSuppressed(this, issue, mCompilationUnit)) { + return; + } + super.report(issue, location, message); + } + + /** + * Reports an issue applicable to a given AST node. The AST node is used as the + * scope to check for suppress lint annotations. + * + * @param issue the issue to report + * @param scope the AST node scope the error applies to. The lint infrastructure + * will check whether there are suppress annotations on this node (or its enclosing + * nodes) and if so suppress the warning without involving the client. + * @param location the location of the issue, or null if not known + * @param message the message for this warning + */ + public void report( + @NonNull Issue issue, + @Nullable Node scope, + @Nullable Location location, + @NonNull String message) { + if (scope != null && mDriver.isSuppressed(this, issue, scope)) { + return; + } + super.report(issue, location, message); + } + + /** + * Report an error. + * Like {@link #report(Issue, Node, Location, String)} but with + * a now-unused data parameter at the end. + * + * @deprecated Use {@link #report(Issue, Node, Location, String)} instead; + * this method is here for custom rule compatibility + */ + @SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules + @Deprecated + public void report( + @NonNull Issue issue, + @Nullable Node scope, + @Nullable Location location, + @NonNull String message, + @SuppressWarnings("UnusedParameters") @Nullable Object data) { + report(issue, scope, location, message); + } + + @Nullable + public static Node findSurroundingMethod(Node scope) { + while (scope != null) { + Class type = scope.getClass(); + // The Lombok AST uses a flat hierarchy of node type implementation classes + // so no need to do instanceof stuff here. + if (type == MethodDeclaration.class || type == ConstructorDeclaration.class) { + return scope; + } + + scope = scope.getParent(); + } + + return null; + } + + @Nullable + public static ClassDeclaration findSurroundingClass(@Nullable Node scope) { + while (scope != null) { + Class type = scope.getClass(); + // The Lombok AST uses a flat hierarchy of node type implementation classes + // so no need to do instanceof stuff here. + if (type == ClassDeclaration.class) { + return (ClassDeclaration) scope; + } + + scope = scope.getParent(); + } + + return null; + } + + @Override + @Nullable + protected String getSuppressCommentPrefix() { + return SUPPRESS_COMMENT_PREFIX; + } + + public boolean isSuppressedWithComment(@NonNull Node scope, @NonNull Issue issue) { + // Check whether there is a comment marker + String contents = getContents(); + assert contents != null; // otherwise we wouldn't be here + Position position = scope.getPosition(); + if (position == null) { + return false; + } + + int start = position.getStart(); + return isSuppressedWithComment(start, issue); + } + + @NonNull + public Location.Handle createLocationHandle(@NonNull Node node) { + return mParser.createLocationHandle(this, node); + } + + @Nullable + public ResolvedNode resolve(@NonNull Node node) { + return mParser.resolve(this, node); + } + + @Nullable + public ResolvedClass findClass(@NonNull String fullyQualifiedName) { + return mParser.findClass(this, fullyQualifiedName); + } + + @Nullable + public TypeDescriptor getType(@NonNull Node node) { + return mParser.getType(this, node); + } + + /** + * Returns true if the given method invocation node corresponds to a call on a + * {@code android.content.Context} + * + * @param node the method call node + * @return true iff the method call is on a class extending context + */ + public boolean isContextMethod(@NonNull MethodInvocation node) { + // Method name used in many other contexts where it doesn't have the + // same semantics; only use this one if we can resolve types + // and we're certain this is the Context method + ResolvedNode resolved = resolve(node); + if (resolved instanceof JavaParser.ResolvedMethod) { + JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod) resolved; + ResolvedClass containingClass = method.getContainingClass(); + if (containingClass.isSubclassOf(CLASS_CONTEXT, false)) { + return true; + } + } + return false; + } + + /** + * Returns the first ancestor node of the given type + * + * @param element the element to search from + * @param clz the target node type + * @param the target node type + * @return the nearest ancestor node in the parent chain, or null if not found + */ + @Nullable + public static T getParentOfType( + @Nullable Node element, + @NonNull Class clz) { + return getParentOfType(element, clz, true); + } + + /** + * Returns the first ancestor node of the given type + * + * @param element the element to search from + * @param clz the target node type + * @param strict if true, do not consider the element itself, only its parents + * @param the target node type + * @return the nearest ancestor node in the parent chain, or null if not found + */ + @Nullable + public static T getParentOfType( + @Nullable Node element, + @NonNull Class clz, + boolean strict) { + if (element == null) { + return null; + } + + if (strict) { + element = element.getParent(); + } + + while (element != null) { + if (clz.isInstance(element)) { + //noinspection unchecked + return (T) element; + } + element = element.getParent(); + } + + return null; + } + + /** + * Returns the first ancestor node of the given type, stopping at the given type + * + * @param element the element to search from + * @param clz the target node type + * @param strict if true, do not consider the element itself, only its parents + * @param terminators optional node types to terminate the search at + * @param the target node type + * @return the nearest ancestor node in the parent chain, or null if not found + */ + @Nullable + public static T getParentOfType(@Nullable Node element, + @NonNull Class clz, + boolean strict, + @NonNull Class... terminators) { + if (element == null) { + return null; + } + if (strict) { + element = element.getParent(); + } + + while (element != null && !clz.isInstance(element)) { + for (Class terminator : terminators) { + if (terminator.isInstance(element)) { + return null; + } + } + element = element.getParent(); + } + + //noinspection unchecked + return (T) element; + } + + /** + * Returns the first sibling of the given node that is of the given class + * + * @param sibling the sibling to search from + * @param clz the type to look for + * @param the type + * @return the first sibling of the given type, or null + */ + @Nullable + public static T getNextSiblingOfType(@Nullable Node sibling, + @NonNull Class clz) { + if (sibling == null) { + return null; + } + Node parent = sibling.getParent(); + if (parent == null) { + return null; + } + + Iterator iterator = parent.getChildren().iterator(); + while (iterator.hasNext()) { + if (iterator.next() == sibling) { + break; + } + } + + while (iterator.hasNext()) { + Node child = iterator.next(); + if (clz.isInstance(child)) { + //noinspection unchecked + return (T) child; + } + + } + + return null; + } + + + /** + * Returns the given argument of the given call + * + * @param call the call containing arguments + * @param index the index of the target argument + * @return the argument at the given index + * @throws IllegalArgumentException if index is outside the valid range + */ + @NonNull + public static Node getArgumentNode(@NonNull MethodInvocation call, int index) { + int i = 0; + for (Expression parameter : call.astArguments()) { + if (i == index) { + return parameter; + } + i++; + } + throw new IllegalArgumentException(Integer.toString(index)); + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LayoutDetector.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LayoutDetector.java new file mode 100644 index 00000000000..b24c1a9d7fb --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LayoutDetector.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2011 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.detector.api; + +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_PADDING; +import static com.android.SdkConstants.ATTR_PADDING_BOTTOM; +import static com.android.SdkConstants.ATTR_PADDING_LEFT; +import static com.android.SdkConstants.ATTR_PADDING_RIGHT; +import static com.android.SdkConstants.ATTR_PADDING_TOP; +import static com.android.SdkConstants.VALUE_FILL_PARENT; +import static com.android.SdkConstants.VALUE_MATCH_PARENT; + +import com.android.annotations.NonNull; +import com.android.resources.ResourceFolderType; +import com.google.common.annotations.Beta; + +import org.w3c.dom.Element; + +/** + * Abstract class specifically intended for layout detectors which provides some + * common utility methods shared by layout detectors. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public abstract class LayoutDetector extends ResourceXmlDetector { + @Override + public boolean appliesTo(@NonNull ResourceFolderType folderType) { + return folderType == ResourceFolderType.LAYOUT; + } + + private static boolean isFillParent(@NonNull Element element, @NonNull String dimension) { + String width = element.getAttributeNS(ANDROID_URI, dimension); + return width.equals(VALUE_MATCH_PARENT) || width.equals(VALUE_FILL_PARENT); + } + + protected static boolean isWidthFillParent(@NonNull Element element) { + return isFillParent(element, ATTR_LAYOUT_WIDTH); + } + + protected static boolean isHeightFillParent(@NonNull Element element) { + return isFillParent(element, ATTR_LAYOUT_HEIGHT); + } + + protected boolean hasPadding(@NonNull Element root) { + return root.hasAttributeNS(ANDROID_URI, ATTR_PADDING) + || root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_LEFT) + || root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_RIGHT) + || root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_TOP) + || root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_BOTTOM); + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LintUtils.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LintUtils.java new file mode 100755 index 00000000000..b233546fe03 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/LintUtils.java @@ -0,0 +1,1265 @@ +/* + * Copyright (C) 2011 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.detector.api; + +import static com.android.SdkConstants.ANDROID_MANIFEST_XML; +import static com.android.SdkConstants.ANDROID_PREFIX; +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_LOCALE; +import static com.android.SdkConstants.BIN_FOLDER; +import static com.android.SdkConstants.DOT_GIF; +import static com.android.SdkConstants.DOT_JPEG; +import static com.android.SdkConstants.DOT_JPG; +import static com.android.SdkConstants.DOT_PNG; +import static com.android.SdkConstants.DOT_WEBP; +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.ID_PREFIX; +import static com.android.SdkConstants.NEW_ID_PREFIX; +import static com.android.SdkConstants.TOOLS_URI; +import static com.android.SdkConstants.UTF_8; +import static com.android.ide.common.resources.configuration.FolderConfiguration.QUALIFIER_SPLITTER; +import static com.android.ide.common.resources.configuration.LocaleQualifier.BCP_47_PREFIX; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.builder.model.AndroidProject; +import com.android.builder.model.ApiVersion; +import com.android.ide.common.rendering.api.ItemResourceValue; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.StyleResourceValue; +import com.android.ide.common.res2.AbstractResourceRepository; +import com.android.ide.common.res2.ResourceItem; +import com.android.ide.common.resources.ResourceUrl; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.common.resources.configuration.LocaleQualifier; +import com.android.resources.FolderTypeRelationship; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkVersionInfo; +import com.android.sdklib.repository.FullRevision; +import com.android.tools.lint.client.api.LintClient; +import com.android.utils.PositionXmlParser; +import com.android.utils.SdkUtils; +import com.google.common.annotations.Beta; +import com.google.common.base.Splitter; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import java.util.Queue; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import lombok.ast.ImportDeclaration; + + +/** + * Useful utility methods related to lint. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public class LintUtils { + // Utility class, do not instantiate + private LintUtils() { + } + + /** + * Format a list of strings, and cut of the list at {@code maxItems} if the + * number of items are greater. + * + * @param strings the list of strings to print out as a comma separated list + * @param maxItems the maximum number of items to print + * @return a comma separated list + */ + @NonNull + public static String formatList(@NonNull List strings, int maxItems) { + StringBuilder sb = new StringBuilder(20 * strings.size()); + + for (int i = 0, n = strings.size(); i < n; i++) { + if (sb.length() > 0) { + sb.append(", "); //$NON-NLS-1$ + } + sb.append(strings.get(i)); + + if (maxItems > 0 && i == maxItems - 1 && n > maxItems) { + sb.append(String.format("... (%1$d more)", n - i - 1)); + break; + } + } + + return sb.toString(); + } + + /** + * Determine if the given type corresponds to a resource that has a unique + * file + * + * @param type the resource type to check + * @return true if the given type corresponds to a file-type resource + */ + public static boolean isFileBasedResourceType(@NonNull ResourceType type) { + List folderTypes = FolderTypeRelationship.getRelatedFolders(type); + for (ResourceFolderType folderType : folderTypes) { + if (folderType != ResourceFolderType.VALUES) { + return type != ResourceType.ID; + } + } + return false; + } + + /** + * Returns true if the given file represents an XML file + * + * @param file the file to be checked + * @return true if the given file is an xml file + */ + public static boolean isXmlFile(@NonNull File file) { + return SdkUtils.endsWithIgnoreCase(file.getPath(), DOT_XML); + } + + /** + * Returns true if the given file represents a bitmap drawable file + * + * @param file the file to be checked + * @return true if the given file is an xml file + */ + public static boolean isBitmapFile(@NonNull File file) { + String path = file.getPath(); + // endsWith(name, DOT_PNG) is also true for endsWith(name, DOT_9PNG) + return endsWith(path, DOT_PNG) + || endsWith(path, DOT_JPG) + || endsWith(path, DOT_GIF) + || endsWith(path, DOT_JPEG) + || endsWith(path, DOT_WEBP); + } + + /** + * Case insensitive ends with + * + * @param string the string to be tested whether it ends with the given + * suffix + * @param suffix the suffix to check + * @return true if {@code string} ends with {@code suffix}, + * case-insensitively. + */ + public static boolean endsWith(@NonNull String string, @NonNull String suffix) { + return string.regionMatches(true /* ignoreCase */, string.length() - suffix.length(), + suffix, 0, suffix.length()); + } + + /** + * Case insensitive starts with + * + * @param string the string to be tested whether it starts with the given prefix + * @param prefix the prefix to check + * @param offset the offset to start checking with + * @return true if {@code string} starts with {@code prefix}, + * case-insensitively. + */ + public static boolean startsWith(@NonNull String string, @NonNull String prefix, int offset) { + return string.regionMatches(true /* ignoreCase */, offset, prefix, 0, prefix.length()); + } + + /** + * Returns the basename of the given filename, unless it's a dot-file such as ".svn". + * + * @param fileName the file name to extract the basename from + * @return the basename (the filename without the file extension) + */ + public static String getBaseName(@NonNull String fileName) { + int extension = fileName.indexOf('.'); + if (extension > 0) { + return fileName.substring(0, extension); + } else { + return fileName; + } + } + + /** + * Returns the children elements of the given node + * + * @param node the parent node + * @return a list of element children, never null + */ + @NonNull + public static List getChildren(@NonNull Node node) { + NodeList childNodes = node.getChildNodes(); + List children = new ArrayList(childNodes.getLength()); + for (int i = 0, n = childNodes.getLength(); i < n; i++) { + Node child = childNodes.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + children.add((Element) child); + } + } + + return children; + } + + /** + * Returns the number of children of the given node + * + * @param node the parent node + * @return the count of element children + */ + public static int getChildCount(@NonNull Node node) { + NodeList childNodes = node.getChildNodes(); + int childCount = 0; + for (int i = 0, n = childNodes.getLength(); i < n; i++) { + Node child = childNodes.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + childCount++; + } + } + + return childCount; + } + + /** + * Returns true if the given element is the root element of its document + * + * @param element the element to test + * @return true if the element is the root element + */ + public static boolean isRootElement(Element element) { + return element == element.getOwnerDocument().getDocumentElement(); + } + + /** + * Returns the given id without an {@code @id/} or {@code @+id} prefix + * + * @param id the id to strip + * @return the stripped id, never null + */ + @NonNull + public static String stripIdPrefix(@Nullable String id) { + if (id == null) { + return ""; + } else if (id.startsWith(NEW_ID_PREFIX)) { + return id.substring(NEW_ID_PREFIX.length()); + } else if (id.startsWith(ID_PREFIX)) { + return id.substring(ID_PREFIX.length()); + } + + return id; + } + + /** + * Returns true if the given two id references match. This is similar to + * String equality, but it also considers "{@code @+id/foo == @id/foo}. + * + * @param id1 the first id to compare + * @param id2 the second id to compare + * @return true if the two id references refer to the same id + */ + public static boolean idReferencesMatch(@Nullable String id1, @Nullable String id2) { + if (id1 == null || id2 == null || id1.isEmpty() || id2.isEmpty()) { + return false; + } + if (id1.startsWith(NEW_ID_PREFIX)) { + if (id2.startsWith(NEW_ID_PREFIX)) { + return id1.equals(id2); + } else { + assert id2.startsWith(ID_PREFIX) : id2; + return ((id1.length() - id2.length()) + == (NEW_ID_PREFIX.length() - ID_PREFIX.length())) + && id1.regionMatches(NEW_ID_PREFIX.length(), id2, + ID_PREFIX.length(), + id2.length() - ID_PREFIX.length()); + } + } else { + assert id1.startsWith(ID_PREFIX) : id1; + if (id2.startsWith(ID_PREFIX)) { + return id1.equals(id2); + } else { + assert id2.startsWith(NEW_ID_PREFIX); + return (id2.length() - id1.length() + == (NEW_ID_PREFIX.length() - ID_PREFIX.length())) + && id2.regionMatches(NEW_ID_PREFIX.length(), id1, + ID_PREFIX.length(), + id1.length() - ID_PREFIX.length()); + } + } + } + + /** + * Computes the edit distance (number of insertions, deletions or substitutions + * to edit one string into the other) between two strings. In particular, + * this will compute the Levenshtein distance. + *

+ * See http://en.wikipedia.org/wiki/Levenshtein_distance for details. + * + * @param s the first string to compare + * @param t the second string to compare + * @return the edit distance between the two strings + */ + public static int editDistance(@NonNull String s, @NonNull String t) { + int m = s.length(); + int n = t.length(); + int[][] d = new int[m + 1][n + 1]; + for (int i = 0; i <= m; i++) { + d[i][0] = i; + } + for (int j = 0; j <= n; j++) { + d[0][j] = j; + } + for (int j = 1; j <= n; j++) { + for (int i = 1; i <= m; i++) { + if (s.charAt(i - 1) == t.charAt(j - 1)) { + d[i][j] = d[i - 1][j - 1]; + } else { + int deletion = d[i - 1][j] + 1; + int insertion = d[i][j - 1] + 1; + int substitution = d[i - 1][j - 1] + 1; + d[i][j] = Math.min(deletion, Math.min(insertion, substitution)); + } + } + } + + return d[m][n]; + } + + /** + * Returns true if assertions are enabled + * + * @return true if assertions are enabled + */ + @SuppressWarnings("all") + public static boolean assertionsEnabled() { + boolean assertionsEnabled = false; + assert assertionsEnabled = true; // Intentional side-effect + return assertionsEnabled; + } + + /** + * Returns the layout resource name for the given layout file + * + * @param layoutFile the file pointing to the layout + * @return the layout resource name, not including the {@code @layout} + * prefix + */ + public static String getLayoutName(File layoutFile) { + String name = layoutFile.getName(); + int dotIndex = name.indexOf('.'); + if (dotIndex != -1) { + name = name.substring(0, dotIndex); + } + return name; + } + + /** + * Splits the given path into its individual parts, attempting to be + * tolerant about path separators (: or ;). It can handle possibly ambiguous + * paths, such as {@code c:\foo\bar:\other}, though of course these are to + * be avoided if possible. + * + * @param path the path variable to split, which can use both : and ; as + * path separators. + * @return the individual path components as an Iterable of strings + */ + public static Iterable splitPath(@NonNull String path) { + if (path.indexOf(';') != -1) { + return Splitter.on(';').omitEmptyStrings().trimResults().split(path); + } + + List combined = new ArrayList(); + Iterables.addAll(combined, Splitter.on(':').omitEmptyStrings().trimResults().split(path)); + for (int i = 0, n = combined.size(); i < n; i++) { + String p = combined.get(i); + if (p.length() == 1 && i < n - 1 && Character.isLetter(p.charAt(0)) + // Technically, Windows paths do not have to have a \ after the :, + // which means it would be using the current directory on that drive, + // but that's unlikely to be the case in a path since it would have + // unpredictable results + && !combined.get(i+1).isEmpty() && combined.get(i+1).charAt(0) == '\\') { + combined.set(i, p + ':' + combined.get(i+1)); + combined.remove(i+1); + n--; + continue; + } + } + + return combined; + } + + /** + * Computes the shared parent among a set of files (which may be null). + * + * @param files the set of files to be checked + * @return the closest common ancestor file, or null if none was found + */ + @Nullable + public static File getCommonParent(@NonNull List files) { + int fileCount = files.size(); + if (fileCount == 0) { + return null; + } else if (fileCount == 1) { + return files.get(0); + } else if (fileCount == 2) { + return getCommonParent(files.get(0), files.get(1)); + } else { + File common = files.get(0); + for (int i = 1; i < fileCount; i++) { + common = getCommonParent(common, files.get(i)); + if (common == null) { + return null; + } + } + + return common; + } + } + + /** + * Computes the closest common parent path between two files. + * + * @param file1 the first file to be compared + * @param file2 the second file to be compared + * @return the closest common ancestor file, or null if the two files have + * no common parent + */ + @Nullable + public static File getCommonParent(@NonNull File file1, @NonNull File file2) { + if (file1.equals(file2)) { + return file1; + } else if (file1.getPath().startsWith(file2.getPath())) { + return file2; + } else if (file2.getPath().startsWith(file1.getPath())) { + return file1; + } else { + // Dumb and simple implementation + File first = file1.getParentFile(); + while (first != null) { + File second = file2.getParentFile(); + while (second != null) { + if (first.equals(second)) { + return first; + } + second = second.getParentFile(); + } + + first = first.getParentFile(); + } + } + return null; + } + + private static final String UTF_16 = "UTF_16"; //$NON-NLS-1$ + private static final String UTF_16LE = "UTF_16LE"; //$NON-NLS-1$ + + /** + * Returns the encoded String for the given file. This is usually the + * same as {@code Files.toString(file, Charsets.UTF8}, but if there's a UTF byte order mark + * (for UTF8, UTF_16 or UTF_16LE), use that instead. + * + * @param client the client to use for I/O operations + * @param file the file to read from + * @return the string + * @throws IOException if the file cannot be read properly + */ + @NonNull + public static String getEncodedString( + @NonNull LintClient client, + @NonNull File file) throws IOException { + byte[] bytes = client.readBytes(file); + if (endsWith(file.getName(), DOT_XML)) { + return PositionXmlParser.getXmlString(bytes); + } + + return getEncodedString(bytes); + } + + /** + * Returns the String corresponding to the given data. This is usually the + * same as {@code new String(data)}, but if there's a UTF byte order mark + * (for UTF8, UTF_16 or UTF_16LE), use that instead. + *

+ * NOTE: For XML files, there is the additional complication that there + * could be a {@code encoding=} attribute in the prologue. For those files, + * use {@link PositionXmlParser#getXmlString(byte[])} instead. + * + * @param data the byte array to construct the string from + * @return the string + */ + @NonNull + public static String getEncodedString(@Nullable byte[] data) { + if (data == null) { + return ""; + } + + int offset = 0; + String defaultCharset = UTF_8; + String charset = null; + // Look for the byte order mark, to see if we need to remove bytes from + // the input stream (and to determine whether files are big endian or little endian) etc + // for files which do not specify the encoding. + // See http://unicode.org/faq/utf_bom.html#BOM for more. + if (data.length > 4) { + if (data[0] == (byte)0xef && data[1] == (byte)0xbb && data[2] == (byte)0xbf) { + // UTF-8 + defaultCharset = charset = UTF_8; + offset += 3; + } else if (data[0] == (byte)0xfe && data[1] == (byte)0xff) { + // UTF-16, big-endian + defaultCharset = charset = UTF_16; + offset += 2; + } else if (data[0] == (byte)0x0 && data[1] == (byte)0x0 + && data[2] == (byte)0xfe && data[3] == (byte)0xff) { + // UTF-32, big-endian + defaultCharset = charset = "UTF_32"; //$NON-NLS-1$ + offset += 4; + } else if (data[0] == (byte)0xff && data[1] == (byte)0xfe + && data[2] == (byte)0x0 && data[3] == (byte)0x0) { + // UTF-32, little-endian. We must check for this *before* looking for + // UTF_16LE since UTF_32LE has the same prefix! + defaultCharset = charset = "UTF_32LE"; //$NON-NLS-1$ + offset += 4; + } else if (data[0] == (byte)0xff && data[1] == (byte)0xfe) { + // UTF-16, little-endian + defaultCharset = charset = UTF_16LE; + offset += 2; + } + } + int length = data.length - offset; + + // Guess encoding by searching for an encoding= entry in the first line. + boolean seenOddZero = false; + boolean seenEvenZero = false; + for (int lineEnd = offset; lineEnd < data.length; lineEnd++) { + if (data[lineEnd] == 0) { + if ((lineEnd - offset) % 2 == 0) { + seenEvenZero = true; + } else { + seenOddZero = true; + } + } else if (data[lineEnd] == '\n' || data[lineEnd] == '\r') { + break; + } + } + + if (charset == null) { + charset = seenOddZero ? UTF_16LE : seenEvenZero ? UTF_16 : UTF_8; + } + + String text = null; + try { + text = new String(data, offset, length, charset); + } catch (UnsupportedEncodingException e) { + try { + if (charset != defaultCharset) { + text = new String(data, offset, length, defaultCharset); + } + } catch (UnsupportedEncodingException u) { + // Just use the default encoding below + } + } + if (text == null) { + text = new String(data, offset, length); + } + return text; + } + + /** + * Returns true if the given class node represents a static inner class. + * + * @param classNode the inner class to be checked + * @return true if the class node represents an inner class that is static + */ + public static boolean isStaticInnerClass(@NonNull ClassNode classNode) { + // Note: We can't just filter out static inner classes like this: + // (classNode.access & Opcodes.ACC_STATIC) != 0 + // because the static flag only appears on methods and fields in the class + // file. Instead, look for the synthetic this pointer. + + @SuppressWarnings("rawtypes") // ASM API + List fieldList = classNode.fields; + for (Object f : fieldList) { + FieldNode field = (FieldNode) f; + if (field.name.startsWith("this$") && (field.access & Opcodes.ACC_SYNTHETIC) != 0) { + return false; + } + } + + return true; + } + + /** + * Returns true if the given class node represents an anonymous inner class + * + * @param classNode the class to be checked + * @return true if the class appears to be an anonymous class + */ + public static boolean isAnonymousClass(@NonNull ClassNode classNode) { + if (classNode.outerClass == null) { + return false; + } + + String name = classNode.name; + int index = name.lastIndexOf('$'); + if (index == -1 || index == name.length() - 1) { + return false; + } + + return Character.isDigit(name.charAt(index + 1)); + } + + /** + * Returns the previous opcode prior to the given node, ignoring label and + * line number nodes + * + * @param node the node to look up the previous opcode for + * @return the previous opcode, or {@link Opcodes#NOP} if no previous node + * was found + */ + public static int getPrevOpcode(@NonNull AbstractInsnNode node) { + AbstractInsnNode prev = getPrevInstruction(node); + if (prev != null) { + return prev.getOpcode(); + } else { + return Opcodes.NOP; + } + } + + /** + * Returns the previous instruction prior to the given node, ignoring label + * and line number nodes. + * + * @param node the node to look up the previous instruction for + * @return the previous instruction, or null if no previous node was found + */ + @Nullable + public static AbstractInsnNode getPrevInstruction(@NonNull AbstractInsnNode node) { + AbstractInsnNode prev = node; + while (true) { + prev = prev.getPrevious(); + if (prev == null) { + return null; + } else { + int type = prev.getType(); + if (type != AbstractInsnNode.LINE && type != AbstractInsnNode.LABEL + && type != AbstractInsnNode.FRAME) { + return prev; + } + } + } + } + + /** + * Returns the next opcode after to the given node, ignoring label and line + * number nodes + * + * @param node the node to look up the next opcode for + * @return the next opcode, or {@link Opcodes#NOP} if no next node was found + */ + public static int getNextOpcode(@NonNull AbstractInsnNode node) { + AbstractInsnNode next = getNextInstruction(node); + if (next != null) { + return next.getOpcode(); + } else { + return Opcodes.NOP; + } + } + + /** + * Returns the next instruction after to the given node, ignoring label and + * line number nodes. + * + * @param node the node to look up the next node for + * @return the next instruction, or null if no next node was found + */ + @Nullable + public static AbstractInsnNode getNextInstruction(@NonNull AbstractInsnNode node) { + AbstractInsnNode next = node; + while (true) { + next = next.getNext(); + if (next == null) { + return null; + } else { + int type = next.getType(); + if (type != AbstractInsnNode.LINE && type != AbstractInsnNode.LABEL + && type != AbstractInsnNode.FRAME) { + return next; + } + } + } + } + + /** + * Returns true if the given directory is a lint manifest file directory. + * + * @param dir the directory to check + * @return true if the directory contains a manifest file + */ + public static boolean isManifestFolder(File dir) { + boolean hasManifest = new File(dir, ANDROID_MANIFEST_XML).exists(); + if (hasManifest) { + // Special case: the bin/ folder can also contain a copy of the + // manifest file, but this is *not* a project directory + if (dir.getName().equals(BIN_FOLDER)) { + // ...unless of course it just *happens* to be a project named bin, in + // which case we peek at its parent to see if this is the case + dir = dir.getParentFile(); + //noinspection ConstantConditions + if (dir != null && isManifestFolder(dir)) { + // Yes, it's a bin/ directory inside a real project: ignore this dir + return false; + } + } + } + + return hasManifest; + } + + /** + * Look up the locale and region from the given parent folder name and + * return it as a combined string, such as "en", "en-rUS", b+eng-US, etc, or null if + * no language is specified. + * + * @param folderName the folder name + * @return the locale+region string or null + */ + @Nullable + public static String getLocaleAndRegion(@NonNull String folderName) { + if (folderName.indexOf('-') == -1) { + return null; + } + + String locale = null; + + for (String qualifier : QUALIFIER_SPLITTER.split(folderName)) { + int qualifierLength = qualifier.length(); + if (qualifierLength == 2) { + char first = qualifier.charAt(0); + char second = qualifier.charAt(1); + if (first >= 'a' && first <= 'z' && second >= 'a' && second <= 'z') { + locale = qualifier; + } + } else if (qualifierLength == 3 && qualifier.charAt(0) == 'r' && locale != null) { + char first = qualifier.charAt(1); + char second = qualifier.charAt(2); + if (first >= 'A' && first <= 'Z' && second >= 'A' && second <= 'Z') { + return locale + '-' + qualifier; + } + break; + } else if (qualifier.startsWith(BCP_47_PREFIX)) { + return qualifier; + } + } + + return locale; + } + + /** + * Returns true if the given class (specified by a fully qualified class + * name) name is imported in the given compilation unit either through a fully qualified + * import or by a wildcard import. + * + * @param compilationUnit the compilation unit + * @param fullyQualifiedName the fully qualified class name + * @return true if the given imported name refers to the given fully + * qualified name + */ + public static boolean isImported( + @Nullable lombok.ast.Node compilationUnit, + @NonNull String fullyQualifiedName) { + if (compilationUnit == null) { + return false; + } + int dotIndex = fullyQualifiedName.lastIndexOf('.'); + int dotLength = fullyQualifiedName.length() - dotIndex; + + boolean imported = false; + for (lombok.ast.Node rootNode : compilationUnit.getChildren()) { + if (rootNode instanceof ImportDeclaration) { + ImportDeclaration importDeclaration = (ImportDeclaration) rootNode; + String fqn = importDeclaration.asFullyQualifiedName(); + if (fqn.equals(fullyQualifiedName)) { + return true; + } else if (fullyQualifiedName.regionMatches(dotIndex, fqn, + fqn.length() - dotLength, dotLength)) { + // This import is importing the class name using some other prefix, so there + // fully qualified class name cannot be imported under that name + return false; + } else if (importDeclaration.astStarImport() + && fqn.regionMatches(0, fqn, 0, dotIndex + 1)) { + imported = true; + // but don't break -- keep searching in case there's a non-wildcard + // import of the specific class name, e.g. if we're looking for + // android.content.SharedPreferences.Editor, don't match on the following: + // import android.content.SharedPreferences.*; + // import foo.bar.Editor; + } + } + } + + return imported; + } + + /** + * Looks up the resource values for the given attribute given a style. Note that + * this only looks project-level style values, it does not resume into the framework + * styles. + */ + @Nullable + public static List getStyleAttributes( + @NonNull Project project, @NonNull LintClient client, + @NonNull String styleUrl, @NonNull String namespace, @NonNull String attribute) { + if (!client.supportsProjectResources()) { + return null; + } + + AbstractResourceRepository resources = client.getProjectResources(project, true); + if (resources == null) { + return null; + } + + ResourceUrl style = ResourceUrl.parse(styleUrl); + if (style == null || style.framework) { + return null; + } + + List result = null; + + Queue queue = new ArrayDeque(); + queue.add(new ResourceValue(style.type, style.name, false)); + Set seen = Sets.newHashSet(); + int count = 0; + boolean isFrameworkAttribute = ANDROID_URI.equals(namespace); + while (count < 30 && !queue.isEmpty()) { + ResourceValue front = queue.remove(); + String name = front.getName(); + seen.add(name); + List items = resources.getResourceItem(front.getResourceType(), name); + if (items != null) { + for (ResourceItem item : items) { + ResourceValue rv = item.getResourceValue(false); + if (rv instanceof StyleResourceValue) { + StyleResourceValue srv = (StyleResourceValue) rv; + ItemResourceValue value = srv.getItem(attribute, isFrameworkAttribute); + if (value != null) { + if (result == null) { + result = Lists.newArrayList(); + } + if (!result.contains(value)) { + result.add(value); + } + } + + String parent = srv.getParentStyle(); + if (parent != null && !parent.startsWith(ANDROID_PREFIX)) { + ResourceUrl p = ResourceUrl.parse(parent); + if (p != null && !p.framework && !seen.contains(p.name)) { + seen.add(p.name); + queue.add(new ResourceValue(ResourceType.STYLE, p.name, + false)); + } + } + + int index = name.lastIndexOf('.'); + if (index > 0) { + String parentName = name.substring(0, index); + if (!seen.contains(parentName)) { + seen.add(parentName); + queue.add(new ResourceValue(ResourceType.STYLE, parentName, + false)); + } + } + } + } + } + + count++; + } + + return result; + } + + @Nullable + public static List getInheritedStyles( + @NonNull Project project, @NonNull LintClient client, + @NonNull String styleUrl) { + if (!client.supportsProjectResources()) { + return null; + } + + AbstractResourceRepository resources = client.getProjectResources(project, true); + if (resources == null) { + return null; + } + + ResourceUrl style = ResourceUrl.parse(styleUrl); + if (style == null || style.framework) { + return null; + } + + List result = null; + + Queue queue = new ArrayDeque(); + queue.add(new ResourceValue(style.type, style.name, false)); + Set seen = Sets.newHashSet(); + int count = 0; + while (count < 30 && !queue.isEmpty()) { + ResourceValue front = queue.remove(); + String name = front.getName(); + seen.add(name); + List items = resources.getResourceItem(front.getResourceType(), name); + if (items != null) { + for (ResourceItem item : items) { + ResourceValue rv = item.getResourceValue(false); + if (rv instanceof StyleResourceValue) { + StyleResourceValue srv = (StyleResourceValue) rv; + if (result == null) { + result = Lists.newArrayList(); + } + result.add(srv); + + String parent = srv.getParentStyle(); + if (parent != null && !parent.startsWith(ANDROID_PREFIX)) { + ResourceUrl p = ResourceUrl.parse(parent); + if (p != null && !p.framework && !seen.contains(p.name)) { + seen.add(p.name); + queue.add(new ResourceValue(ResourceType.STYLE, p.name, + false)); + } + } + + int index = name.lastIndexOf('.'); + if (index > 0) { + String parentName = name.substring(0, index); + if (!seen.contains(parentName)) { + seen.add(parentName); + queue.add(new ResourceValue(ResourceType.STYLE, parentName, + false)); + } + } + } + } + } + + count++; + } + + return result; + } + + /** Returns true if the given two paths point to the same logical resource file within + * a source set. This means that it only checks the parent folder name and individual + * file name, not the path outside the parent folder. + * + * @param file1 the first file to compare + * @param file2 the second file to compare + * @return true if the two files have the same parent and file names + */ + public static boolean isSameResourceFile(@Nullable File file1, @Nullable File file2) { + if (file1 != null && file2 != null + && file1.getName().equals(file2.getName())) { + File parent1 = file1.getParentFile(); + File parent2 = file2.getParentFile(); + if (parent1 != null && parent2 != null && + parent1.getName().equals(parent2.getName())) { + return true; + } + } + + return false; + } + + /** + * Whether we should attempt to look up the prefix from the model. Set to false + * if we encounter a model which is too old. + *

+ * This is public such that code which for example syncs to a new gradle model + * can reset it. + */ + public static boolean sTryPrefixLookup = true; + + /** Looks up the resource prefix for the given Gradle project, if possible */ + @Nullable + public static String computeResourcePrefix(@Nullable AndroidProject project) { + try { + if (sTryPrefixLookup && project != null) { + return project.getResourcePrefix(); + } + } catch (Exception e) { + // This happens if we're talking to an older model than 0.10 + // Ignore; fall through to normal handling and never try again. + //noinspection AssignmentToStaticFieldFromInstanceMethod + sTryPrefixLookup = false; + } + + return null; + } + + /** Computes a suggested name given a resource prefix and resource name */ + public static String computeResourceName(@NonNull String prefix, @NonNull String name) { + if (prefix.isEmpty()) { + return name; + } else if (name.isEmpty()) { + return prefix; + } else if (prefix.endsWith("_")) { + return prefix + name; + } else { + return prefix + Character.toUpperCase(name.charAt(0)) + name.substring(1); + } + } + + + /** + * Convert an {@link com.android.builder.model.ApiVersion} to a {@link + * com.android.sdklib.AndroidVersion}. The chief problem here is that the {@link + * com.android.builder.model.ApiVersion}, when using a codename, will not encode the + * corresponding API level (it just reflects the string entered by the user in the gradle file) + * so we perform a search here (since lint really wants to know the actual numeric API level) + * + * @param api the api version to convert + * @param targets if known, the installed targets (used to resolve platform codenames, only + * needed to resolve platforms newer than the tools since {@link + * com.android.sdklib.SdkVersionInfo} knows the rest) + * @return the corresponding version + */ + @NonNull + public static AndroidVersion convertVersion( + @NonNull ApiVersion api, + @Nullable IAndroidTarget[] targets) { + String codename = api.getCodename(); + if (codename != null) { + AndroidVersion version = SdkVersionInfo.getVersion(codename, targets); + if (version != null) { + return version; + } + return new AndroidVersion(api.getApiLevel(), codename); + } + return new AndroidVersion(api.getApiLevel(), null); + } + + /** + * Returns true if the given Gradle model is older than the given version number + */ + public static boolean isModelOlderThan(@Nullable AndroidProject project, + int major, int minor, int micro) { + if (project != null) { + String modelVersion = project.getModelVersion(); + try { + FullRevision version = FullRevision.parseRevision(modelVersion); + if (version.getMajor() != major) { + return version.getMajor() < major; + } + if (version.getMinor() != minor) { + return version.getMinor() < minor; + } + return version.getMicro() < micro; + } catch (NumberFormatException e) { + // ignore + } + } + + return false; + } + + /** + * Looks for a certain string within a larger string, which should immediately follow + * the given prefix and immediately precede the given suffix. + * + * @param string the full string to search + * @param prefix the optional prefix to follow + * @param suffix the optional suffix to precede + * @return the corresponding substring, if present + */ + @Nullable + public static String findSubstring(@NonNull String string, @Nullable String prefix, + @Nullable String suffix) { + int start = 0; + if (prefix != null) { + start = string.indexOf(prefix); + if (start == -1) { + return null; + } + start += prefix.length(); + } + + if (suffix != null) { + int end = string.indexOf(suffix, start); + if (end == -1) { + return null; + } + return string.substring(start, end); + } + + return string.substring(start); + } + + /** + * Splits up the given message coming from a given string format (where the string + * format follows the very specific convention of having only strings formatted exactly + * with the format %n$s where n is between 1 and 9 inclusive, and each formatting parameter + * appears exactly once, and in increasing order. + * + * @param format the format string responsible for creating the error message + * @param errorMessage an error message formatted with the format string + * @return the specific values inserted into the format + */ + @NonNull + public static List getFormattedParameters( + @NonNull String format, + @NonNull String errorMessage) { + StringBuilder pattern = new StringBuilder(format.length()); + int parameter = 1; + for (int i = 0, n = format.length(); i < n; i++) { + char c = format.charAt(i); + if (c == '%') { + // Only support formats of the form %n$s where n is 1 <= n <=9 + assert i < format.length() - 4 : format; + assert format.charAt(i + 1) == ('0' + parameter) : format; + assert Character.isDigit(format.charAt(i + 1)) : format; + assert format.charAt(i + 2) == '$' : format; + assert format.charAt(i + 3) == 's' : format; + parameter++; + i += 3; + pattern.append("(.*)"); + } else { + pattern.append(c); + } + } + try { + Pattern compile = Pattern.compile(pattern.toString()); + Matcher matcher = compile.matcher(errorMessage); + if (matcher.find()) { + int groupCount = matcher.groupCount(); + List parameters = Lists.newArrayListWithExpectedSize(groupCount); + for (int i = 1; i <= groupCount; i++) { + parameters.add(matcher.group(i)); + } + + return parameters; + } + + } catch (PatternSyntaxException pse) { + // Internal error: string format is not valid. Should be caught by unit tests + // as a failure to return the formatted parameters. + } + return Collections.emptyList(); + } + + /** + * Escapes the given property file value (right hand side of property assignment) + * as required by the property file format (e.g. escapes colons and backslashes) + * + * @param value the value to be escaped + * @return the escaped value + */ + @NonNull + public static String escapePropertyValue(@NonNull String value) { + // Slow, stupid implementation, but is 100% compatible with Java's property file + // implementation + Properties properties = new Properties(); + properties.setProperty("k", value); // key doesn't matter + StringWriter writer = new StringWriter(); + try { + properties.store(writer, null); + String s = writer.toString(); + int end = s.length(); + + // Writer inserts trailing newline + String lineSeparator = SdkUtils.getLineSeparator(); + if (s.endsWith(lineSeparator)) { + end -= lineSeparator.length(); + } + + int start = s.indexOf('='); + assert start != -1 : s; + return s.substring(start + 1, end); + } + catch (IOException e) { + return value; // shouldn't happen; we're not going to disk + } + } + + /** + * Returns the locale for the given parent folder. + * + * @param parent the name of the parent folder + * @return null if the locale is not known, or a locale qualifier providing the language + * and possibly region + */ + @Nullable + public static LocaleQualifier getLocale(@NonNull String parent) { + if (parent.indexOf('-') != -1) { + FolderConfiguration config = FolderConfiguration.getConfigForFolder(parent); + if (config != null) { + return config.getLocaleQualifier(); + } + } + return null; + } + + /** + * Returns the locale for the given context. + * + * @param context the context to look up the locale for + * @return null if the locale is not known, or a locale qualifier providing the language + * and possibly region + */ + @Nullable + public static LocaleQualifier getLocale(@NonNull XmlContext context) { + Element root = context.document.getDocumentElement(); + if (root != null) { + String locale = root.getAttributeNS(TOOLS_URI, ATTR_LOCALE); + if (locale != null && !locale.isEmpty()) { + return getLocale(locale); + } + } + + return getLocale(context.file.getParentFile().getName()); + } + + /** + * Check whether the given resource file is in an English locale + * @param context the XML context for the resource file + * @param assumeForBase whether the base folder (e.g. no locale specified) should be + * treated as English + */ + public static boolean isEnglishResource(@NonNull XmlContext context, boolean assumeForBase) { + LocaleQualifier locale = LintUtils.getLocale(context); + if (locale == null) { + return assumeForBase; + } else { + return "en".equals(locale.getLanguage()); //$NON-NLS-1$ + } + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Location.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Location.java new file mode 100644 index 00000000000..284e41b0968 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Location.java @@ -0,0 +1,777 @@ +/* + * Copyright (C) 2011 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.detector.api; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.blame.SourcePosition; +import com.android.ide.common.res2.ResourceFile; +import com.android.ide.common.res2.ResourceItem; +import com.google.common.annotations.Beta; + +import java.io.File; + +/** + * Location information for a warning + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public class Location { + private static final String SUPER_KEYWORD = "super"; //$NON-NLS-1$ + + private final File mFile; + private final Position mStart; + private final Position mEnd; + private String mMessage; + private Location mSecondary; + private Object mClientData; + + /** + * (Private constructor, use one of the factory methods + * {@link Location#create(File)}, + * {@link Location#create(File, Position, Position)}, or + * {@link Location#create(File, String, int, int)}. + *

+ * Constructs a new location range for the given file, from start to end. If + * the length of the range is not known, end may be null. + * + * @param file the associated file (but see the documentation for + * {@link #getFile()} for more information on what the file + * represents) + * @param start the starting position, or null + * @param end the ending position, or null + */ + protected Location(@NonNull File file, @Nullable Position start, @Nullable Position end) { + super(); + mFile = file; + mStart = start; + mEnd = end; + } + + /** + * Returns the file containing the warning. Note that the file *itself* may + * not yet contain the error. When editing a file in the IDE for example, + * the tool could generate warnings in the background even before the + * document is saved. However, the file is used as a identifying token for + * the document being edited, and the IDE integration can map this back to + * error locations in the editor source code. + * + * @return the file handle for the location + */ + @NonNull + public File getFile() { + return mFile; + } + + /** + * The start position of the range + * + * @return the start position of the range, or null + */ + @Nullable + public Position getStart() { + return mStart; + } + + /** + * The end position of the range + * + * @return the start position of the range, may be null for an empty range + */ + @Nullable + public Position getEnd() { + return mEnd; + } + + /** + * Returns a secondary location associated with this location (if + * applicable), or null. + * + * @return a secondary location or null + */ + @Nullable + public Location getSecondary() { + return mSecondary; + } + + /** + * Sets a secondary location for this location. + * + * @param secondary a secondary location associated with this location + */ + public void setSecondary(@Nullable Location secondary) { + mSecondary = secondary; + } + + /** + * Sets a custom message for this location. This is typically used for + * secondary locations, to describe the significance of this alternate + * location. For example, for a duplicate id warning, the primary location + * might say "This is a duplicate id", pointing to the second occurrence of + * id declaration, and then the secondary location could point to the + * original declaration with the custom message "Originally defined here". + * + * @param message the message to apply to this location + */ + public void setMessage(@NonNull String message) { + mMessage = message; + } + + /** + * Returns the custom message for this location, if any. This is typically + * used for secondary locations, to describe the significance of this + * alternate location. For example, for a duplicate id warning, the primary + * location might say "This is a duplicate id", pointing to the second + * occurrence of id declaration, and then the secondary location could point + * to the original declaration with the custom message + * "Originally defined here". + * + * @return the custom message for this location, or null + */ + @Nullable + public String getMessage() { + return mMessage; + } + + /** + * Sets the client data associated with this location. This is an optional + * field which can be used by the creator of the {@link Location} to store + * temporary state associated with the location. + * + * @param clientData the data to store with this location + */ + public void setClientData(@Nullable Object clientData) { + mClientData = clientData; + } + + /** + * Returns the client data associated with this location - an optional field + * which can be used by the creator of the {@link Location} to store + * temporary state associated with the location. + * + * @return the data associated with this location + */ + @Nullable + public Object getClientData() { + return mClientData; + } + + @Override + public String toString() { + return "Location [file=" + mFile + ", start=" + mStart + ", end=" + mEnd + ", message=" + + mMessage + ']'; + } + + /** + * Creates a new location for the given file + * + * @param file the file to create a location for + * @return a new location + */ + @NonNull + public static Location create(@NonNull File file) { + return new Location(file, null /*start*/, null /*end*/); + } + + /** + * Creates a new location for the given file and SourcePosition. + * + * @param file the file containing the positions + * @param position the source position + * @return a new location + */ + @NonNull + public static Location create( + @NonNull File file, + @NonNull SourcePosition position) { + if (position.equals(SourcePosition.UNKNOWN)) { + return new Location(file, null /*start*/, null /*end*/); + } + return new Location(file, + new DefaultPosition( + position.getStartLine(), + position.getStartColumn(), + position.getStartOffset()), + new DefaultPosition( + position.getEndLine(), + position.getEndColumn(), + position.getEndOffset())); + } + + /** + * Creates a new location for the given file and starting and ending + * positions. + * + * @param file the file containing the positions + * @param start the starting position + * @param end the ending position + * @return a new location + */ + @NonNull + public static Location create( + @NonNull File file, + @NonNull Position start, + @Nullable Position end) { + return new Location(file, start, end); + } + + /** + * Creates a new location for the given file, with the given contents, for + * the given offset range. + * + * @param file the file containing the location + * @param contents the current contents of the file + * @param startOffset the starting offset + * @param endOffset the ending offset + * @return a new location + */ + @NonNull + public static Location create( + @NonNull File file, + @Nullable String contents, + int startOffset, + int endOffset) { + if (startOffset < 0 || endOffset < startOffset) { + throw new IllegalArgumentException("Invalid offsets"); + } + + if (contents == null) { + return new Location(file, + new DefaultPosition(-1, -1, startOffset), + new DefaultPosition(-1, -1, endOffset)); + } + + int size = contents.length(); + endOffset = Math.min(endOffset, size); + startOffset = Math.min(startOffset, endOffset); + Position start = null; + int line = 0; + int lineOffset = 0; + char prev = 0; + for (int offset = 0; offset <= size; offset++) { + if (offset == startOffset) { + start = new DefaultPosition(line, offset - lineOffset, offset); + } + if (offset == endOffset) { + Position end = new DefaultPosition(line, offset - lineOffset, offset); + return new Location(file, start, end); + } + char c = contents.charAt(offset); + if (c == '\n') { + lineOffset = offset + 1; + if (prev != '\r') { + line++; + } + } else if (c == '\r') { + line++; + lineOffset = offset + 1; + } + prev = c; + } + return create(file); + } + + /** + * Creates a new location for the given file, with the given contents, for + * the given line number. + * + * @param file the file containing the location + * @param contents the current contents of the file + * @param line the line number (0-based) for the position + * @return a new location + */ + @NonNull + public static Location create(@NonNull File file, @NonNull String contents, int line) { + return create(file, contents, line, null, null, null); + } + + /** + * Creates a new location for the given file, with the given contents, for + * the given line number. + * + * @param file the file containing the location + * @param contents the current contents of the file + * @param line the line number (0-based) for the position + * @param patternStart an optional pattern to search for from the line + * match; if found, adjust the column and offsets to begin at the + * pattern start + * @param patternEnd an optional pattern to search for behind the start + * pattern; if found, adjust the end offset to match the end of + * the pattern + * @param hints optional additional information regarding the pattern search + * @return a new location + */ + @NonNull + public static Location create(@NonNull File file, @NonNull String contents, int line, + @Nullable String patternStart, @Nullable String patternEnd, + @Nullable SearchHints hints) { + int currentLine = 0; + int offset = 0; + while (currentLine < line) { + offset = contents.indexOf('\n', offset); + if (offset == -1) { + return create(file); + } + currentLine++; + offset++; + } + + if (line == currentLine) { + if (patternStart != null) { + SearchDirection direction = SearchDirection.NEAREST; + if (hints != null) { + direction = hints.mDirection; + } + + int index; + if (direction == SearchDirection.BACKWARD) { + index = findPreviousMatch(contents, offset, patternStart, hints); + line = adjustLine(contents, line, offset, index); + } else if (direction == SearchDirection.EOL_BACKWARD) { + int lineEnd = contents.indexOf('\n', offset); + if (lineEnd == -1) { + lineEnd = contents.length(); + } + + index = findPreviousMatch(contents, lineEnd, patternStart, hints); + line = adjustLine(contents, line, offset, index); + } else if (direction == SearchDirection.FORWARD) { + index = findNextMatch(contents, offset, patternStart, hints); + line = adjustLine(contents, line, offset, index); + } else { + assert direction == SearchDirection.NEAREST; + + int before = findPreviousMatch(contents, offset, patternStart, hints); + int after = findNextMatch(contents, offset, patternStart, hints); + + if (before == -1) { + index = after; + line = adjustLine(contents, line, offset, index); + } else if (after == -1) { + index = before; + line = adjustLine(contents, line, offset, index); + } else if (offset - before < after - offset) { + index = before; + line = adjustLine(contents, line, offset, index); + } else { + index = after; + line = adjustLine(contents, line, offset, index); + } + } + + if (index != -1) { + int lineStart = contents.lastIndexOf('\n', index); + if (lineStart == -1) { + lineStart = 0; + } else { + lineStart++; // was pointing to the previous line's CR, not line start + } + int column = index - lineStart; + if (patternEnd != null) { + int end = contents.indexOf(patternEnd, offset + patternStart.length()); + if (end != -1) { + return new Location(file, new DefaultPosition(line, column, index), + new DefaultPosition(line, -1, end + patternEnd.length())); + } + } else if (hints != null && (hints.isJavaSymbol() || hints.isWholeWord())) { + if (hints.isConstructor() && contents.startsWith(SUPER_KEYWORD, index)) { + patternStart = SUPER_KEYWORD; + } + return new Location(file, new DefaultPosition(line, column, index), + new DefaultPosition(line, column + patternStart.length(), + index + patternStart.length())); + } + return new Location(file, new DefaultPosition(line, column, index), + new DefaultPosition(line, column, index + patternStart.length())); + } + } + + Position position = new DefaultPosition(line, -1, offset); + return new Location(file, position, position); + } + + return create(file); + } + + private static int findPreviousMatch(@NonNull String contents, int offset, String pattern, + @Nullable SearchHints hints) { + while (true) { + int index = contents.lastIndexOf(pattern, offset); + if (index == -1) { + return -1; + } else { + if (isMatch(contents, index, pattern, hints)) { + return index; + } else { + offset = index - pattern.length(); + } + } + } + } + + private static int findNextMatch(@NonNull String contents, int offset, String pattern, + @Nullable SearchHints hints) { + int constructorIndex = -1; + if (hints != null && hints.isConstructor()) { + // Special condition: See if the call is referenced as "super" instead. + assert hints.isWholeWord(); + int index = contents.indexOf(SUPER_KEYWORD, offset); + if (index != -1 && isMatch(contents, index, SUPER_KEYWORD, hints)) { + constructorIndex = index; + } + } + + while (true) { + int index = contents.indexOf(pattern, offset); + if (index == -1) { + return constructorIndex; + } else { + if (isMatch(contents, index, pattern, hints)) { + if (constructorIndex != -1) { + return Math.min(constructorIndex, index); + } + return index; + } else { + offset = index + pattern.length(); + } + } + } + } + + private static boolean isMatch(@NonNull String contents, int offset, String pattern, + @Nullable SearchHints hints) { + if (!contents.startsWith(pattern, offset)) { + return false; + } + + if (hints != null) { + char prevChar = offset > 0 ? contents.charAt(offset - 1) : 0; + int lastIndex = offset + pattern.length() - 1; + char nextChar = lastIndex < contents.length() - 1 ? contents.charAt(lastIndex + 1) : 0; + + if (hints.isWholeWord() && (Character.isLetter(prevChar) + || Character.isLetter(nextChar))) { + return false; + + } + + if (hints.isJavaSymbol()) { + if (Character.isJavaIdentifierPart(prevChar) + || Character.isJavaIdentifierPart(nextChar)) { + return false; + } + + if (prevChar == '"') { + return false; + } + + // TODO: Additional validation to see if we're in a comment, string, etc. + // This will require lexing from the beginning of the buffer. + } + + if (hints.isConstructor() && SUPER_KEYWORD.equals(pattern)) { + // Only looking for super(), not super.x, so assert that the next + // non-space character is ( + int index = lastIndex + 1; + while (index < contents.length() - 1) { + char c = contents.charAt(index); + if (c == '(') { + break; + } else if (!Character.isWhitespace(c)) { + return false; + } + index++; + } + } + } + + return true; + } + + private static int adjustLine(String doc, int line, int offset, int newOffset) { + if (newOffset == -1) { + return line; + } + + if (newOffset < offset) { + return line - countLines(doc, newOffset, offset); + } else { + return line + countLines(doc, offset, newOffset); + } + } + + private static int countLines(String doc, int start, int end) { + int lines = 0; + for (int offset = start; offset < end; offset++) { + char c = doc.charAt(offset); + if (c == '\n') { + lines++; + } + } + + return lines; + } + + /** + * Reverses the secondary location list initiated by the given location + * + * @param location the first location in the list + * @return the first location in the reversed list + */ + public static Location reverse(@NonNull Location location) { + Location next = location.getSecondary(); + location.setSecondary(null); + while (next != null) { + Location nextNext = next.getSecondary(); + next.setSecondary(location); + location = next; + next = nextNext; + } + + return location; + } + + /** + * A {@link Handle} is a reference to a location. The point of a location + * handle is to be able to create them cheaply, and then resolve them into + * actual locations later (if needed). This makes it possible to for example + * delay looking up line numbers, for locations that are offset based. + */ + public interface Handle { + /** + * Compute a full location for the given handle + * + * @return create a location for this handle + */ + @NonNull + Location resolve(); + + /** + * Sets the client data associated with this location. This is an optional + * field which can be used by the creator of the {@link Location} to store + * temporary state associated with the location. + * + * @param clientData the data to store with this location + */ + void setClientData(@Nullable Object clientData); + + /** + * Returns the client data associated with this location - an optional field + * which can be used by the creator of the {@link Location} to store + * temporary state associated with the location. + * + * @return the data associated with this location + */ + @Nullable + Object getClientData(); + } + + /** A default {@link Handle} implementation for simple file offsets */ + public static class DefaultLocationHandle implements Handle { + private final File mFile; + private final String mContents; + private final int mStartOffset; + private final int mEndOffset; + private Object mClientData; + + /** + * Constructs a new {@link DefaultLocationHandle} + * + * @param context the context pointing to the file and its contents + * @param startOffset the start offset within the file + * @param endOffset the end offset within the file + */ + public DefaultLocationHandle(@NonNull Context context, int startOffset, int endOffset) { + mFile = context.file; + mContents = context.getContents(); + mStartOffset = startOffset; + mEndOffset = endOffset; + } + + @Override + @NonNull + public Location resolve() { + return create(mFile, mContents, mStartOffset, mEndOffset); + } + + @Override + public void setClientData(@Nullable Object clientData) { + mClientData = clientData; + } + + @Override + @Nullable + public Object getClientData() { + return mClientData; + } + } + + public static class ResourceItemHandle implements Handle { + private final ResourceItem mItem; + + public ResourceItemHandle(@NonNull ResourceItem item) { + mItem = item; + } + @NonNull + @Override + public Location resolve() { + // TODO: Look up the exact item location more + // closely + ResourceFile source = mItem.getSource(); + assert source != null : mItem; + return create(source.getFile()); + } + + @Override + public void setClientData(@Nullable Object clientData) { + } + + @Nullable + @Override + public Object getClientData() { + return null; + } + } + + /** + * Whether to look forwards, or backwards, or in both directions, when + * searching for a pattern in the source code to determine the right + * position range for a given symbol. + *

+ * When dealing with bytecode for example, there are only line number entries + * within method bodies, so when searching for the method declaration, we should only + * search backwards from the first line entry in the method. + */ + public enum SearchDirection { + /** Only search forwards */ + FORWARD, + + /** Only search backwards */ + BACKWARD, + + /** Search backwards from the current end of line (normally it's the beginning of + * the current line) */ + EOL_BACKWARD, + + /** + * Search both forwards and backwards from the given line, and prefer + * the match that is closest + */ + NEAREST, + } + + /** + * Extra information pertaining to finding a symbol in a source buffer, + * used by {@link Location#create(File, String, int, String, String, SearchHints)} + */ + public static class SearchHints { + /** + * the direction to search for the nearest match in (provided + * {@code patternStart} is non null) + */ + @NonNull + private final SearchDirection mDirection; + + /** Whether the matched pattern should be a whole word */ + private boolean mWholeWord; + + /** + * Whether the matched pattern should be a Java symbol (so for example, + * a match inside a comment or string literal should not be used) + */ + private boolean mJavaSymbol; + + /** + * Whether the matched pattern corresponds to a constructor; if so, look for + * some other possible source aliases too, such as "super". + */ + private boolean mConstructor; + + private SearchHints(@NonNull SearchDirection direction) { + super(); + mDirection = direction; + } + + /** + * Constructs a new {@link SearchHints} object + * + * @param direction the direction to search in for the pattern + * @return a new @link SearchHints} object + */ + @NonNull + public static SearchHints create(@NonNull SearchDirection direction) { + return new SearchHints(direction); + } + + /** + * Indicates that pattern matches should apply to whole words only + + * @return this, for constructor chaining + */ + @NonNull + public SearchHints matchWholeWord() { + mWholeWord = true; + + return this; + } + + /** @return true if the pattern match should be for whole words only */ + public boolean isWholeWord() { + return mWholeWord; + } + + /** + * Indicates that pattern matches should apply to Java symbols only + * + * @return this, for constructor chaining + */ + @NonNull + public SearchHints matchJavaSymbol() { + mJavaSymbol = true; + mWholeWord = true; + + return this; + } + + /** @return true if the pattern match should be for Java symbols only */ + public boolean isJavaSymbol() { + return mJavaSymbol; + } + + /** + * Indicates that pattern matches should apply to constructors. If so, look for + * some other possible source aliases too, such as "super". + * + * @return this, for constructor chaining + */ + @NonNull + public SearchHints matchConstructor() { + mConstructor = true; + mWholeWord = true; + mJavaSymbol = true; + + return this; + } + + /** @return true if the pattern match should be for a constructor */ + public boolean isConstructor() { + return mConstructor; + } + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Position.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Position.java new file mode 100644 index 00000000000..6407cc9df5d --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Position.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2011 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.detector.api; + +import com.google.common.annotations.Beta; + +/** + * Information about a position in a file/document. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public abstract class Position { + /** + * Returns the line number (0-based where the first line is line 0) + * + * @return the 0-based line number + */ + public abstract int getLine(); + + /** + * The character offset + * + * @return the 0-based character offset + */ + public abstract int getOffset(); + + /** + * Returns the column number (where the first character on the line is 0), + * or -1 if unknown + * + * @return the 0-based column number + */ + public abstract int getColumn(); +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Project.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Project.java new file mode 100755 index 00000000000..9141b9feaaa --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Project.java @@ -0,0 +1,1348 @@ +/* + * Copyright (C) 2011 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.detector.api; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.build.FilterData; +import com.android.build.OutputFile; +import com.android.builder.model.*; +import com.android.ide.common.repository.ResourceVisibilityLookup; +import com.android.resources.Density; +import com.android.resources.ResourceFolderType; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkVersionInfo; +import com.android.tools.lint.client.api.CircularDependencyException; +import com.android.tools.lint.client.api.Configuration; +import com.android.tools.lint.client.api.LintClient; +import com.android.tools.lint.client.api.SdkInfo; +import com.google.common.annotations.Beta; +import com.google.common.base.CharMatcher; +import com.google.common.base.Charsets; +import com.google.common.base.Splitter; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.common.io.Closeables; +import com.google.common.io.Files; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.android.SdkConstants.*; +import static com.android.sdklib.SdkVersionInfo.HIGHEST_KNOWN_API; + +/** + * A project contains information about an Android project being scanned for + * Lint errors. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public class Project { + protected final LintClient mClient; + protected final File mDir; + protected final File mReferenceDir; + protected Configuration mConfiguration; + protected String mPackage; + protected int mBuildSdk = -1; + protected IAndroidTarget mTarget; + + protected AndroidVersion mManifestMinSdk = AndroidVersion.DEFAULT; + protected AndroidVersion mManifestTargetSdk = AndroidVersion.DEFAULT; + + protected boolean mLibrary; + protected String mName; + protected String mProguardPath; + protected boolean mMergeManifests; + + /** The SDK info, if any */ + protected SdkInfo mSdkInfo; + + /** + * If non null, specifies a non-empty list of specific files under this + * project which should be checked. + */ + protected List mFiles; + protected List mProguardFiles; + protected List mGradleFiles; + protected List mManifestFiles; + protected List mJavaSourceFolders; + protected List mJavaClassFolders; + protected List mJavaLibraries; + protected List mTestSourceFolders; + protected List mResourceFolders; + protected List mDirectLibraries; + protected List mAllLibraries; + protected boolean mReportIssues = true; + protected Boolean mGradleProject; + protected Boolean mSupportLib; + protected Boolean mAppCompat; + private Map mSuperClassMap; + private ResourceVisibilityLookup mResourceVisibility; + + /** + * Creates a new {@link Project} for the given directory. + * + * @param client the tool running the lint check + * @param dir the root directory of the project + * @param referenceDir See {@link #getReferenceDir()}. + * @return a new {@link Project} + */ + @NonNull + public static Project create( + @NonNull LintClient client, + @NonNull File dir, + @NonNull File referenceDir) { + return new Project(client, dir, referenceDir); + } + + /** + * Returns true if this project is a Gradle-based Android project + * + * @return true if this is a Gradle-based project + */ + public boolean isGradleProject() { + if (mGradleProject == null) { + mGradleProject = mClient.isGradleProject(this); + } + + return mGradleProject; + } + + /** + * Returns true if this project is an Android project. + * + * @return true if this project is an Android project. + */ + @SuppressWarnings("MethodMayBeStatic") + public boolean isAndroidProject() { + return true; + } + + /** + * Returns the project model for this project if it corresponds to + * a Gradle project. This is the case if {@link #isGradleProject()} + * is true and {@link #isLibrary()} is false. + * + * @return the project model, or null + */ + @Nullable + public AndroidProject getGradleProjectModel() { + return null; + } + + /** + * Returns the project model for this project if it corresponds to + * a Gradle library. This is the case if both + * {@link #isGradleProject()} and {@link #isLibrary()} return true. + * + * @return the project model, or null + */ + @SuppressWarnings("UnusedDeclaration") + @Nullable + public AndroidLibrary getGradleLibraryModel() { + return null; + } + + /** + * Returns the current selected variant, if any (and if the current project is a Gradle + * project). This can be used by incremental lint rules to warn about problems in the current + * context. Lint rules should however strive to perform cross variant analysis and warn about + * problems in any configuration. + * + * @return the select variant, or null + */ + @Nullable + public Variant getCurrentVariant() { + return null; + } + + /** Creates a new Project. Use one of the factory methods to create. */ + protected Project( + @NonNull LintClient client, + @NonNull File dir, + @NonNull File referenceDir) { + mClient = client; + mDir = dir; + mReferenceDir = referenceDir; + initialize(); + } + + protected void initialize() { + // Default initialization: Use ADT/ant style project.properties file + try { + // Read properties file and initialize library state + Properties properties = new Properties(); + File propFile = new File(mDir, PROJECT_PROPERTIES); + if (propFile.exists()) { + BufferedInputStream is = new BufferedInputStream(new FileInputStream(propFile)); + try { + properties.load(is); + String value = properties.getProperty(ANDROID_LIBRARY); + mLibrary = VALUE_TRUE.equals(value); + String proguardPath = properties.getProperty(PROGUARD_CONFIG); + if (proguardPath != null) { + mProguardPath = proguardPath; + } + mMergeManifests = VALUE_TRUE.equals(properties.getProperty( + "manifestmerger.enabled")); //$NON-NLS-1$ + String target = properties.getProperty("target"); //$NON-NLS-1$ + if (target != null) { + int index = target.lastIndexOf('-'); + if (index == -1) { + index = target.lastIndexOf(':'); + } + if (index != -1) { + String versionString = target.substring(index + 1); + try { + mBuildSdk = Integer.parseInt(versionString); + } catch (NumberFormatException nufe) { + mClient.log(Severity.WARNING, null, + "Unexpected build target format: %1$s", target); + } + } + } + + for (int i = 1; i < 1000; i++) { + String key = String.format(ANDROID_LIBRARY_REFERENCE_FORMAT, i); + String library = properties.getProperty(key); + if (library == null || library.isEmpty()) { + // No holes in the numbering sequence is allowed + break; + } + + File libraryDir = new File(mDir, library).getCanonicalFile(); + + if (mDirectLibraries == null) { + mDirectLibraries = new ArrayList(); + } + + // Adjust the reference dir to be a proper prefix path of the + // library dir + File libraryReferenceDir = mReferenceDir; + if (!libraryDir.getPath().startsWith(mReferenceDir.getPath())) { + // Symlinks etc might have been resolved, so do those to + // the reference dir as well + libraryReferenceDir = libraryReferenceDir.getCanonicalFile(); + if (!libraryDir.getPath().startsWith(mReferenceDir.getPath())) { + File file = libraryReferenceDir; + while (file != null && !file.getPath().isEmpty()) { + if (libraryDir.getPath().startsWith(file.getPath())) { + libraryReferenceDir = file; + break; + } + file = file.getParentFile(); + } + } + } + + try { + Project libraryPrj = mClient.getProject(libraryDir, + libraryReferenceDir); + mDirectLibraries.add(libraryPrj); + // By default, we don't report issues in inferred library projects. + // The driver will set report = true for those library explicitly + // requested. + libraryPrj.setReportIssues(false); + } catch (CircularDependencyException e) { + e.setProject(this); + e.setLocation(Location.create(propFile)); + throw e; + } + } + } finally { + try { + Closeables.close(is, true /* swallowIOException */); + } catch (IOException e) { + // cannot happen + } + } + } + } catch (IOException ioe) { + mClient.log(ioe, "Initializing project state"); + } + + if (mDirectLibraries != null) { + mDirectLibraries = Collections.unmodifiableList(mDirectLibraries); + } else { + mDirectLibraries = Collections.emptyList(); + } + } + + @Override + public String toString() { + return "Project [dir=" + mDir + ']'; + } + + @Override + public int hashCode() { + return mDir.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Project other = (Project) obj; + return mDir.equals(other.mDir); + } + + /** + * Adds the given file to the list of files which should be checked in this + * project. If no files are added, the whole project will be checked. + * + * @param file the file to be checked + */ + public void addFile(@NonNull File file) { + if (mFiles == null) { + mFiles = new ArrayList(); + } + mFiles.add(file); + } + + /** + * The list of files to be checked in this project. If null, the whole + * project should be checked. + * + * @return the subset of files to be checked, or null for the whole project + */ + @Nullable + public List getSubset() { + return mFiles; + } + + /** + * Returns the list of source folders for Java source files + * + * @return a list of source folders to search for .java files + */ + @NonNull + public List getJavaSourceFolders() { + if (mJavaSourceFolders == null) { + if (isAospFrameworksProject(mDir)) { + return Collections.singletonList(new File(mDir, "java")); //$NON-NLS-1$ + } + if (isAospBuildEnvironment()) { + String top = getAospTop(); + if (mDir.getAbsolutePath().startsWith(top)) { + mJavaSourceFolders = getAospJavaSourcePath(); + return mJavaSourceFolders; + } + } + + mJavaSourceFolders = mClient.getJavaSourceFolders(this); + } + + return mJavaSourceFolders; + } + + /** + * Returns the list of output folders for class files + * @return a list of output folders to search for .class files + */ + @NonNull + public List getJavaClassFolders() { + if (mJavaClassFolders == null) { + if (isAospFrameworksProject(mDir)) { + File top = mDir.getParentFile().getParentFile().getParentFile(); + if (top != null) { + File out = new File(top, "out"); + if (out.exists()) { + String relative = + "target/common/obj/JAVA_LIBRARIES/framework_intermediates/classes.jar"; + File jar = new File(out, relative.replace('/', File.separatorChar)); + if (jar.exists()) { + mJavaClassFolders = Collections.singletonList(jar); + return mJavaClassFolders; + } + } + } + } + if (isAospBuildEnvironment()) { + String top = getAospTop(); + if (mDir.getAbsolutePath().startsWith(top)) { + mJavaClassFolders = getAospJavaClassPath(); + return mJavaClassFolders; + } + } + + mJavaClassFolders = mClient.getJavaClassFolders(this); + } + return mJavaClassFolders; + } + + /** + * Returns the list of Java libraries (typically .jar files) that this + * project depends on. Note that this refers to jar libraries, not Android + * library projects which are processed in a separate pass with their own + * source and class folders. + * + * @return a list of .jar files (or class folders) that this project depends + * on. + */ + @NonNull + public List getJavaLibraries() { + if (mJavaLibraries == null) { + // AOSP builds already merge libraries and class folders into + // the single classes.jar file, so these have already been processed + // in getJavaClassFolders. + + mJavaLibraries = mClient.getJavaLibraries(this); + } + + return mJavaLibraries; + } + + /** + * Returns the list of source folders for Java test source files + * + * @return a list of source folders to search for .java files + */ + @NonNull + public List getTestSourceFolders() { + if (mTestSourceFolders == null) { + mTestSourceFolders = mClient.getTestSourceFolders(this); + } + + return mTestSourceFolders; + } + + /** + * Returns the resource folder. + * + * @return a file pointing to the resource folder, or null if the project + * does not contain any resources + */ + @NonNull + public List getResourceFolders() { + if (mResourceFolders == null) { + List folders = mClient.getResourceFolders(this); + + if (folders.size() == 1 && isAospFrameworksProject(mDir)) { + // No manifest file for this project: just init the manifest values here + mManifestMinSdk = mManifestTargetSdk = new AndroidVersion(HIGHEST_KNOWN_API, null); + File folder = new File(folders.get(0), RES_FOLDER); + if (!folder.exists()) { + folders = Collections.emptyList(); + } + } + + mResourceFolders = folders; + } + + return mResourceFolders; + + } + + /** + * Returns the relative path of a given file relative to the user specified + * directory (which is often the project directory but sometimes a higher up + * directory when a directory tree is being scanned + * + * @param file the file under this project to check + * @return the path relative to the reference directory (often the project directory) + */ + @NonNull + public String getDisplayPath(@NonNull File file) { + String path = file.getPath(); + String referencePath = mReferenceDir.getPath(); + if (path.startsWith(referencePath)) { + int length = referencePath.length(); + if (path.length() > length && path.charAt(length) == File.separatorChar) { + length++; + } + + return path.substring(length); + } + + return path; + } + + /** + * Returns the relative path of a given file within the current project. + * + * @param file the file under this project to check + * @return the path relative to the project + */ + @NonNull + public String getRelativePath(@NonNull File file) { + String path = file.getPath(); + String referencePath = mDir.getPath(); + if (path.startsWith(referencePath)) { + int length = referencePath.length(); + if (path.length() > length && path.charAt(length) == File.separatorChar) { + length++; + } + + return path.substring(length); + } + + return path; + } + + /** + * Returns the project root directory + * + * @return the dir + */ + @NonNull + public File getDir() { + return mDir; + } + + /** + * Returns the original user supplied directory where the lint search + * started. For example, if you run lint against {@code /tmp/foo}, and it + * finds a project to lint in {@code /tmp/foo/dev/src/project1}, then the + * {@code dir} is {@code /tmp/foo/dev/src/project1} and the + * {@code referenceDir} is {@code /tmp/foo/}. + * + * @return the reference directory, never null + */ + @NonNull + public File getReferenceDir() { + return mReferenceDir; + } + + /** + * Gets the configuration associated with this project + * + * @return the configuration associated with this project + */ + @NonNull + public Configuration getConfiguration() { + if (mConfiguration == null) { + mConfiguration = mClient.getConfiguration(this); + } + return mConfiguration; + } + + /** + * Returns the application package specified by the manifest + * + * @return the application package, or null if unknown + */ + @Nullable + public String getPackage() { + //assert !mLibrary; // Should call getPackage on the master project, not the library + // Assertion disabled because you might be running lint on a standalone library project. + + return mPackage; + } + + /** + * Returns the minimum API level for the project + * + * @return the minimum API level or {@link AndroidVersion#DEFAULT} if unknown + */ + @NonNull + public AndroidVersion getMinSdkVersion() { + return mManifestMinSdk == null ? AndroidVersion.DEFAULT : mManifestMinSdk; + } + + /** + * Returns the minimum API level requested by the manifest, or -1 if not + * specified. Use {@link #getMinSdkVersion()} to get a full version if you need + * to check if the platform is a preview platform etc. + * + * @return the minimum API level or -1 if unknown + */ + public int getMinSdk() { + AndroidVersion version = getMinSdkVersion(); + return version == AndroidVersion.DEFAULT ? -1 : version.getApiLevel(); + } + + /** + * Returns the target API level for the project + * + * @return the target API level or {@link AndroidVersion#DEFAULT} if unknown + */ + @NonNull + public AndroidVersion getTargetSdkVersion() { + return mManifestTargetSdk == AndroidVersion.DEFAULT + ? getMinSdkVersion() : mManifestTargetSdk; + } + + /** + * Returns the target API level specified by the manifest, or -1 if not + * specified. Use {@link #getTargetSdkVersion()} to get a full version if you need + * to check if the platform is a preview platform etc. + * + * @return the target API level or -1 if unknown + */ + public int getTargetSdk() { + AndroidVersion version = getTargetSdkVersion(); + return version == AndroidVersion.DEFAULT ? -1 : version.getApiLevel(); + } + + /** + * Returns the target API used to build the project, or -1 if not known + * + * @return the build target API or -1 if unknown + */ + public int getBuildSdk() { + return mBuildSdk; + } + + /** + * Returns the target used to build the project, or null if not known + * + * @return the build target, or null + */ + @Nullable + public IAndroidTarget getBuildTarget() { + if (mTarget == null) { + mTarget = mClient.getCompileTarget(this); + } + + return mTarget; + } + + /** + * Initialized the manifest state from the given manifest model + * + * @param document the DOM document for the manifest XML document + */ + public void readManifest(@NonNull Document document) { + Element root = document.getDocumentElement(); + if (root == null) { + return; + } + + mPackage = root.getAttribute(ATTR_PACKAGE); + + // Treat support libraries as non-reportable (in Eclipse where we don't + // have binary libraries, the support libraries have to be source-copied into + // the workspace which then triggers warnings in these libraries that users + // shouldn't have to investigate) + if (mPackage != null && mPackage.startsWith("android.support.")) { + mReportIssues = false; + } + + // Initialize minSdk and targetSdk + mManifestMinSdk = AndroidVersion.DEFAULT; + mManifestTargetSdk = AndroidVersion.DEFAULT; + NodeList usesSdks = root.getElementsByTagName(TAG_USES_SDK); + if (usesSdks.getLength() > 0) { + Element element = (Element) usesSdks.item(0); + + String minSdk = null; + if (element.hasAttributeNS(ANDROID_URI, ATTR_MIN_SDK_VERSION)) { + minSdk = element.getAttributeNS(ANDROID_URI, ATTR_MIN_SDK_VERSION); + } + if (minSdk != null) { + IAndroidTarget[] targets = mClient.getTargets(); + mManifestMinSdk = SdkVersionInfo.getVersion(minSdk, targets); + } + + if (element.hasAttributeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION)) { + String targetSdk = element.getAttributeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION); + if (targetSdk != null) { + IAndroidTarget[] targets = mClient.getTargets(); + mManifestTargetSdk = SdkVersionInfo.getVersion(targetSdk, targets); + } + } else { + mManifestTargetSdk = mManifestMinSdk; + } + } else if (isAospBuildEnvironment()) { + extractAospMinSdkVersion(); + mManifestTargetSdk = mManifestMinSdk; + } + } + + /** + * Returns true if this project is an Android library project + * + * @return true if this project is an Android library project + */ + public boolean isLibrary() { + return mLibrary; + } + + /** + * Returns the list of library projects referenced by this project + * + * @return the list of library projects referenced by this project, never + * null + */ + @NonNull + public List getDirectLibraries() { + return mDirectLibraries != null ? mDirectLibraries : Collections.emptyList(); + } + + /** + * Returns the transitive closure of the library projects for this project + * + * @return the transitive closure of the library projects for this project + */ + @NonNull + public List getAllLibraries() { + if (mAllLibraries == null) { + if (mDirectLibraries.isEmpty()) { + return mDirectLibraries; + } + + List all = new ArrayList(); + Set seen = Sets.newHashSet(); + Set path = Sets.newHashSet(); + seen.add(this); + path.add(this); + addLibraryProjects(all, seen, path); + mAllLibraries = all; + } + + return mAllLibraries; + } + + /** + * Adds this project's library project and their library projects + * recursively into the given collection of projects + * + * @param collection the collection to add the projects into + * @param seen full set of projects we've processed + * @param path the current path of library dependencies followed + */ + private void addLibraryProjects(@NonNull Collection collection, + @NonNull Set seen, @NonNull Set path) { + for (Project library : mDirectLibraries) { + if (seen.contains(library)) { + if (path.contains(library)) { + mClient.log(Severity.WARNING, null, + "Internal lint error: cyclic library dependency for %1$s", library); + } + continue; + } + collection.add(library); + seen.add(library); + path.add(library); + // Recurse + library.addLibraryProjects(collection, seen, path); + path.remove(library); + } + } + + /** + * Gets the SDK info for the current project. + * + * @return the SDK info for the current project, never null + */ + @NonNull + public SdkInfo getSdkInfo() { + if (mSdkInfo == null) { + mSdkInfo = mClient.getSdkInfo(this); + } + + return mSdkInfo; + } + + /** + * Gets the paths to the manifest files in this project, if any exists. The manifests + * should be provided such that the main manifest comes first, then any flavor versions, + * then any build types. + * + * @return the path to the manifest file, or null if it does not exist + */ + @NonNull + public List getManifestFiles() { + if (mManifestFiles == null) { + File manifestFile = new File(mDir, ANDROID_MANIFEST_XML); + if (manifestFile.exists()) { + mManifestFiles = Collections.singletonList(manifestFile); + } else { + mManifestFiles = Collections.emptyList(); + } + } + + return mManifestFiles; + } + + /** + * Returns the proguard files configured for this project, if any + * + * @return the proguard files, if any + */ + @NonNull + public List getProguardFiles() { + if (mProguardFiles == null) { + List files = new ArrayList(); + if (mProguardPath != null) { + Splitter splitter = Splitter.on(CharMatcher.anyOf(":;")); //$NON-NLS-1$ + for (String path : splitter.split(mProguardPath)) { + if (path.contains("${")) { //$NON-NLS-1$ + // Don't analyze the global/user proguard files + continue; + } + File file = new File(path); + if (!file.isAbsolute()) { + file = new File(getDir(), path); + } + if (file.exists()) { + files.add(file); + } + } + } + if (files.isEmpty()) { + File file = new File(getDir(), OLD_PROGUARD_FILE); + if (file.exists()) { + files.add(file); + } + file = new File(getDir(), FN_PROJECT_PROGUARD_FILE); + if (file.exists()) { + files.add(file); + } + } + mProguardFiles = files; + } + return mProguardFiles; + } + + /** + * Returns the Gradle build script files configured for this project, if any + * + * @return the Gradle files, if any + */ + @NonNull + public List getGradleBuildScripts() { + if (mGradleFiles == null) { + if (isGradleProject()) { + mGradleFiles = Lists.newArrayListWithExpectedSize(2); + File build = new File(mDir, SdkConstants.FN_BUILD_GRADLE); + if (build.exists()) { + mGradleFiles.add(build); + } + File settings = new File(mDir, SdkConstants.FN_SETTINGS_GRADLE); + if (settings.exists()) { + mGradleFiles.add(settings); + } + } else { + mGradleFiles = Collections.emptyList(); + } + } + + return mGradleFiles; + } + + /** + * Returns the name of the project + * + * @return the name of the project, never null + */ + @NonNull + public String getName() { + if (mName == null) { + mName = mClient.getProjectName(this); + } + + return mName; + } + + /** + * Sets the name of the project + * + * @param name the name of the project, never null + */ + public void setName(@NonNull String name) { + assert !name.isEmpty(); + mName = name; + } + + /** + * Sets whether lint should report issues in this project. See + * {@link #getReportIssues()} for a full description of what that means. + * + * @param reportIssues whether lint should report issues in this project + */ + public void setReportIssues(boolean reportIssues) { + mReportIssues = reportIssues; + } + + /** + * Returns whether lint should report issues in this project. + *

+ * If a user specifies a project and its library projects for analysis, then + * those library projects are all "included", and all errors found in all + * the projects are reported. But if the user is only running lint on the + * main project, we shouldn't report errors in any of the library projects. + * We still need to consider them for certain types of checks, such + * as determining whether resources found in the main project are unused, so + * the detectors must still get a chance to look at these projects. The + * {@code #getReportIssues()} attribute is used for this purpose. + * + * @return whether lint should report issues in this project + */ + public boolean getReportIssues() { + return mReportIssues; + } + + /** + * Returns whether manifest merging is in effect + * + * @return true if manifests in library projects should be merged into main projects + */ + public boolean isMergingManifests() { + return mMergeManifests; + } + + + // --------------------------------------------------------------------------- + // Support for running lint on the AOSP source tree itself + + private static Boolean sAospBuild; + + /** Is lint running in an AOSP build environment */ + private static boolean isAospBuildEnvironment() { + if (sAospBuild == null) { + sAospBuild = getAospTop() != null; + } + + return sAospBuild; + } + + /** + * Is this the frameworks AOSP project? Needs some hardcoded support since + * it doesn't have a manifest file, etc. + * + * @param dir the project directory to check + * @return true if this looks like the frameworks/base/core project + */ + public static boolean isAospFrameworksProject(@NonNull File dir) { + if (!dir.getPath().endsWith("core")) { //$NON-NLS-1$ + return false; + } + + File parent = dir.getParentFile(); + if (parent == null || !parent.getName().equals("base")) { //$NON-NLS-1$ + return false; + } + + parent = parent.getParentFile(); + //noinspection RedundantIfStatement + if (parent == null || !parent.getName().equals("frameworks")) { //$NON-NLS-1$ + return false; + } + + return true; + } + + /** Get the root AOSP dir, if any */ + private static String getAospTop() { + return System.getenv("ANDROID_BUILD_TOP"); //$NON-NLS-1$ + } + + /** Get the host out directory in AOSP, if any */ + private static String getAospHostOut() { + return System.getenv("ANDROID_HOST_OUT"); //$NON-NLS-1$ + } + + /** Get the product out directory in AOSP, if any */ + private static String getAospProductOut() { + return System.getenv("ANDROID_PRODUCT_OUT"); //$NON-NLS-1$ + } + + private List getAospJavaSourcePath() { + List sources = new ArrayList(2); + // Normal sources + File src = new File(mDir, "src"); //$NON-NLS-1$ + if (src.exists()) { + sources.add(src); + } + + // Generates sources + for (File dir : getIntermediateDirs()) { + File classes = new File(dir, "src"); //$NON-NLS-1$ + if (classes.exists()) { + sources.add(classes); + } + } + + if (sources.isEmpty()) { + mClient.log(null, + "Warning: Could not find sources or generated sources for project %1$s", + getName()); + } + + return sources; + } + + private List getAospJavaClassPath() { + List classDirs = new ArrayList(1); + + for (File dir : getIntermediateDirs()) { + File classes = new File(dir, "classes"); //$NON-NLS-1$ + if (classes.exists()) { + classDirs.add(classes); + } else { + classes = new File(dir, "classes.jar"); //$NON-NLS-1$ + if (classes.exists()) { + classDirs.add(classes); + } + } + } + + if (classDirs.isEmpty()) { + mClient.log(null, + "No bytecode found: Has the project been built? (%1$s)", getName()); + } + + return classDirs; + } + + /** Find the _intermediates directories for a given module name */ + private List getIntermediateDirs() { + // See build/core/definitions.mk and in particular the "intermediates-dir-for" definition + List intermediates = new ArrayList(); + + // TODO: Look up the module name, e.g. LOCAL_MODULE. However, + // some Android.mk files do some complicated things with it - and most + // projects use the same module name as the directory name. + String moduleName = mDir.getName(); + + String top = getAospTop(); + final String[] outFolders = new String[] { + top + "/out/host/common/obj", //$NON-NLS-1$ + top + "/out/target/common/obj", //$NON-NLS-1$ + getAospHostOut() + "/obj", //$NON-NLS-1$ + getAospProductOut() + "/obj" //$NON-NLS-1$ + }; + final String[] moduleClasses = new String[] { + "APPS", //$NON-NLS-1$ + "JAVA_LIBRARIES", //$NON-NLS-1$ + }; + + for (String out : outFolders) { + assert new File(out.replace('/', File.separatorChar)).exists() : out; + for (String moduleClass : moduleClasses) { + String path = out + '/' + moduleClass + '/' + moduleName + + "_intermediates"; //$NON-NLS-1$ + File file = new File(path.replace('/', File.separatorChar)); + if (file.exists()) { + intermediates.add(file); + } + } + } + + return intermediates; + } + + private void extractAospMinSdkVersion() { + // Is the SDK level specified by a Makefile? + boolean found = false; + File makefile = new File(mDir, "Android.mk"); //$NON-NLS-1$ + if (makefile.exists()) { + try { + List lines = Files.readLines(makefile, Charsets.UTF_8); + Pattern p = Pattern.compile("LOCAL_SDK_VERSION\\s*:=\\s*(.*)"); //$NON-NLS-1$ + for (String line : lines) { + line = line.trim(); + Matcher matcher = p.matcher(line); + if (matcher.matches()) { + found = true; + String version = matcher.group(1); + if (version.equals("current")) { //$NON-NLS-1$ + mManifestMinSdk = findCurrentAospVersion(); + } else { + mManifestMinSdk = SdkVersionInfo.getVersion(version, + mClient.getTargets()); + } + break; + } + } + } catch (IOException ioe) { + mClient.log(ioe, null); + } + } + + if (!found) { + mManifestMinSdk = findCurrentAospVersion(); + } + } + + /** Cache for {@link #findCurrentAospVersion()} */ + private static AndroidVersion sCurrentVersion; + + /** In an AOSP build environment, identify the currently built image version, if available */ + private static AndroidVersion findCurrentAospVersion() { + if (sCurrentVersion == null) { + File apiDir = new File(getAospTop(), "frameworks/base/api" //$NON-NLS-1$ + .replace('/', File.separatorChar)); + File[] apiFiles = apiDir.listFiles(); + if (apiFiles == null) { + sCurrentVersion = AndroidVersion.DEFAULT; + return sCurrentVersion; + } + int max = 1; + for (File apiFile : apiFiles) { + String name = apiFile.getName(); + int index = name.indexOf('.'); + if (index > 0) { + String base = name.substring(0, index); + if (Character.isDigit(base.charAt(0))) { + try { + int version = Integer.parseInt(base); + if (version > max) { + max = version; + } + } catch (NumberFormatException nufe) { + // pass + } + } + } + } + sCurrentVersion = new AndroidVersion(max, null); + } + + return sCurrentVersion; + } + + /** + * Returns true if this project depends on the given artifact. Note that + * the project doesn't have to be a Gradle project; the artifact is just + * an identifier for name a specific library, such as com.android.support:support-v4 + * to identify the support library + * + * @param artifact the Gradle/Maven name of a library + * @return true if the library is installed, false if it is not, and null if + * we're not sure + */ + @Nullable + public Boolean dependsOn(@NonNull String artifact) { + if (SUPPORT_LIB_ARTIFACT.equals(artifact)) { + if (mSupportLib == null) { + for (File file : getJavaLibraries()) { + String name = file.getName(); + if (name.equals("android-support-v4.jar") //$NON-NLS-1$ + || name.startsWith("support-v4-")) { //$NON-NLS-1$ + mSupportLib = true; + break; + } + } + if (mSupportLib == null) { + for (Project dependency : getDirectLibraries()) { + Boolean b = dependency.dependsOn(artifact); + if (b != null && b) { + mSupportLib = true; + break; + } + } + } + if (mSupportLib == null) { + mSupportLib = false; + } + } + + return mSupportLib; + } else if (APPCOMPAT_LIB_ARTIFACT.equals(artifact)) { + if (mAppCompat == null) { + for (File file : getJavaLibraries()) { + String name = file.getName(); + if (name.startsWith("appcompat-v7-")) { //$NON-NLS-1$ + mAppCompat = true; + break; + } + } + if (mAppCompat == null) { + for (Project dependency : getDirectLibraries()) { + Boolean b = dependency.dependsOn(artifact); + if (b != null && b) { + mAppCompat = true; + break; + } + } + } + if (mAppCompat == null) { + mAppCompat = false; + } + } + + return mAppCompat; + } + + return null; + } + + private List mCachedApplicableDensities; + + /** + * Returns the set of applicable densities for this project. If null, there are no density + * restrictions and all densities apply. + * + * @return the list of specific densities that apply in this project, or null if all densities + * apply + */ + @Nullable + public List getApplicableDensities() { + if (mCachedApplicableDensities == null) { + // Use the gradle API to set up relevant densities. For example, if the + // build.gradle file contains this: + // android { + // defaultConfig { + // resConfigs "nodpi", "hdpi" + // } + // } + // ...then we should only enforce hdpi densities, not all these others! + if (isGradleProject() && getGradleProjectModel() != null && + getCurrentVariant() != null) { + Set relevantDensities = Sets.newHashSet(); + Variant variant = getCurrentVariant(); + List variantFlavors = variant.getProductFlavors(); + AndroidProject gradleProjectModel = getGradleProjectModel(); + + addResConfigsFromFlavor(relevantDensities, null, + getGradleProjectModel().getDefaultConfig()); + for (ProductFlavorContainer container : gradleProjectModel.getProductFlavors()) { + addResConfigsFromFlavor(relevantDensities, variantFlavors, container); + } + + // Are there any splits that specify densities? + if (relevantDensities.isEmpty()) { + AndroidArtifact mainArtifact = variant.getMainArtifact(); + Collection outputs = mainArtifact.getOutputs(); + for (AndroidArtifactOutput output : outputs) { + for (OutputFile file : output.getOutputs()) { + final String DENSITY_NAME = OutputFile.FilterType.DENSITY.name(); + if (file.getFilterTypes().contains(DENSITY_NAME)) { + for (FilterData data : file.getFilters()) { + if (DENSITY_NAME.equals(data.getFilterType())) { + relevantDensities.add(data.getIdentifier()); + } + } + } + } + } + } + + if (!relevantDensities.isEmpty()) { + mCachedApplicableDensities = Lists.newArrayListWithExpectedSize(10); + for (String density : relevantDensities) { + String folder = ResourceFolderType.DRAWABLE.getName() + '-' + density; + mCachedApplicableDensities.add(folder); + } + Collections.sort(mCachedApplicableDensities); + } else { + mCachedApplicableDensities = Collections.emptyList(); + } + } else { + mCachedApplicableDensities = Collections.emptyList(); + } + } + + return mCachedApplicableDensities.isEmpty() ? null : mCachedApplicableDensities; + } + + /** + * Returns a super class map for this project. The keys and values are internal + * class names (e.g. java/lang/Integer, not java.lang.Integer). + * @return a map, possibly empty but never null + */ + @NonNull + public Map getSuperClassMap() { + if (mSuperClassMap == null) { + mSuperClassMap = mClient.createSuperClassMap(this); + } + + return mSuperClassMap; + } + + /** + * Adds in the resConfig values specified by the given flavor container, assuming + * it's in one of the relevant variantFlavors, into the given set + */ + private static void addResConfigsFromFlavor(@NonNull Set relevantDensities, + @Nullable List variantFlavors, + @NonNull ProductFlavorContainer container) { + ProductFlavor flavor = container.getProductFlavor(); + if (variantFlavors == null || variantFlavors.contains(flavor.getName())) { + if (!flavor.getResourceConfigurations().isEmpty()) { + for (String densityName : flavor.getResourceConfigurations()) { + Density density = Density.getEnum(densityName); + if (density != null && density.isRecommended() + && density != Density.NODPI && density != Density.ANYDPI) { + relevantDensities.add(densityName); + } + } + } + } + } + + /** + * Returns a shared {@link ResourceVisibilityLookup} + * + * @return a shared provider for looking up resource visibility + */ + @NonNull + public ResourceVisibilityLookup getResourceVisibility() { + if (mResourceVisibility == null) { + if (isGradleProject()) { + AndroidProject project = getGradleProjectModel(); + Variant variant = getCurrentVariant(); + if (project != null && variant != null) { + mResourceVisibility = mClient.getResourceVisibilityProvider().get(project, + variant); + + } else if (getGradleLibraryModel() != null) { + try { + mResourceVisibility = mClient.getResourceVisibilityProvider() + .get(getGradleLibraryModel()); + } catch (Exception ignore) { + // Handle talking to older Gradle plugins (where we don't + // have access to the model version to check up front + } + } + } + if (mResourceVisibility == null) { + mResourceVisibility = ResourceVisibilityLookup.NONE; + } + } + + return mResourceVisibility; + } + + /** + * Returns the associated client + * + * @return the client + */ + @NonNull + public LintClient getClient() { + return mClient; + } + + /** + * Returns the compile target to use for this project + * + * @return the compile target to use to build this project + */ + @Nullable + public IAndroidTarget getCompileTarget() { + return mClient.getCompileTarget(this); + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceContext.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceContext.java new file mode 100644 index 00000000000..3fdb6b786da --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceContext.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2014 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.detector.api; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.resources.ResourceFolderType; +import com.android.tools.lint.client.api.LintDriver; +import com.google.common.annotations.Beta; + +import java.io.File; + +/** + * A {@link com.android.tools.lint.detector.api.Context} used when checking resource files + * (both bitmaps and XML files; for XML files a subclass of this context is used: + * {@link com.android.tools.lint.detector.api.XmlContext}.) + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public class ResourceContext extends Context { + private final ResourceFolderType mFolderType; + + /** + * Construct a new {@link com.android.tools.lint.detector.api.ResourceContext} + * + * @param driver the driver running through the checks + * @param project the project containing the file being checked + * @param main the main project if this project is a library project, or + * null if this is not a library project. The main project is + * the root project of all library projects, not necessarily the + * directly including project. + * @param file the file being checked + * @param folderType the {@link com.android.resources.ResourceFolderType} of this file, if any + */ + public ResourceContext( + @NonNull LintDriver driver, + @NonNull Project project, + @Nullable Project main, + @NonNull File file, + @Nullable ResourceFolderType folderType) { + super(driver, project, main, file); + mFolderType = folderType; + } + + /** + * Returns the resource folder type of this XML file, if any. + * + * @return the resource folder type or null + */ + @Nullable + public ResourceFolderType getResourceFolderType() { + return mFolderType; + } + + /** + * Returns the folder version. For example, for the file values-v14/foo.xml, + * it returns 14. + * + * @return the folder version, or -1 if no specific version was specified + */ + public int getFolderVersion() { + return mDriver.getResourceFolderVersion(file); + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceXmlDetector.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceXmlDetector.java new file mode 100644 index 00000000000..bd574071701 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/ResourceXmlDetector.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2011 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.detector.api; + +import com.android.annotations.NonNull; +import com.android.resources.ResourceFolderType; +import com.google.common.annotations.Beta; + +import java.io.File; + +/** + * Specialized detector intended for XML resources. Detectors that apply to XML + * resources should extend this detector instead since it provides special + * iteration hooks that are more efficient. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public abstract class ResourceXmlDetector extends Detector implements Detector.XmlScanner { + @Override + public boolean appliesTo(@NonNull Context context, @NonNull File file) { + return LintUtils.isXmlFile(file); + } + + /** + * Returns whether this detector applies to the given folder type. This + * allows the detectors to be pruned from iteration, so for example when we + * are analyzing a string value file we don't need to look up detectors + * related to layout. + * + * @param folderType the folder type to be visited + * @return true if this detector can apply to resources in folders of the + * given type + */ + @Override + public boolean appliesTo(@NonNull ResourceFolderType folderType) { + return true; + } + + @Override + public void run(@NonNull Context context) { + // The infrastructure should never call this method on an xml detector since + // it will run the various visitors instead + assert false; + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Scope.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Scope.java new file mode 100644 index 00000000000..55dd7783644 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Scope.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2011 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.detector.api; + +import static com.android.SdkConstants.ANDROID_MANIFEST_XML; +import static com.android.SdkConstants.DOT_CLASS; +import static com.android.SdkConstants.DOT_GRADLE; +import static com.android.SdkConstants.DOT_JAVA; +import static com.android.SdkConstants.DOT_PNG; +import static com.android.SdkConstants.DOT_PROPERTIES; +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE; +import static com.android.SdkConstants.OLD_PROGUARD_FILE; +import static com.android.SdkConstants.RES_FOLDER; + +import com.android.annotations.NonNull; +import com.google.common.annotations.Beta; + +import java.io.File; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; + +/** + * The scope of a detector is the set of files a detector must consider when + * performing its analysis. This can be used to determine when issues are + * potentially obsolete, whether a detector should re-run on a file save, etc. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public enum Scope { + /** + * The analysis only considers a single XML resource file at a time. + *

+ * Issues which are only affected by a single resource file can be checked + * for incrementally when a file is edited. + */ + RESOURCE_FILE, + + /** + * The analysis only considers a single binary (typically a bitmap) resource file at a time. + *

+ * Issues which are only affected by a single resource file can be checked + * for incrementally when a file is edited. + */ + BINARY_RESOURCE_FILE, + + /** + * The analysis considers the resource folders + */ + RESOURCE_FOLDER, + + /** + * The analysis considers all the resource file. This scope must not + * be used in conjunction with {@link #RESOURCE_FILE}; an issue scope is + * either considering just a single resource file or all the resources, not + * both. + */ + ALL_RESOURCE_FILES, + + /** + * The analysis only considers a single Java source file at a time. + *

+ * Issues which are only affected by a single Java source file can be + * checked for incrementally when a Java source file is edited. + */ + JAVA_FILE, + + /** + * The analysis considers all the Java source files together. + *

+ * This flag is mutually exclusive with {@link #JAVA_FILE}. + */ + ALL_JAVA_FILES, + + /** + * The analysis only considers a single Java class file at a time. + *

+ * Issues which are only affected by a single Java class file can be checked + * for incrementally when a Java source file is edited and then recompiled. + */ + CLASS_FILE, + + /** + * The analysis considers all the Java class files together. + *

+ * This flag is mutually exclusive with {@link #CLASS_FILE}. + */ + ALL_CLASS_FILES, + + /** The analysis considers the manifest file */ + MANIFEST, + + /** The analysis considers the Proguard configuration file */ + PROGUARD_FILE, + + /** + * The analysis considers classes in the libraries for this project. These + * will be analyzed before the classes themselves. + */ + JAVA_LIBRARIES, + + /** The analysis considers a Gradle build file */ + GRADLE_FILE, + + /** The analysis considers Java property files */ + PROPERTY_FILE, + + /** The analysis considers test sources as well */ + TEST_SOURCES, + + /** + * Scope for other files. Issues that specify a custom scope will be called unconditionally. + * This will call {@link Detector#run(Context)}} on the detectors unconditionally. + */ + OTHER; + + /** + * Returns true if the given scope set corresponds to scanning a single file + * rather than a whole project + * + * @param scopes the scope set to check + * @return true if the scope set references a single file + */ + public static boolean checkSingleFile(@NonNull EnumSet scopes) { + int size = scopes.size(); + if (size == 2) { + // When single checking a Java source file, we check both its Java source + // and the associated class files + return scopes.contains(JAVA_FILE) && scopes.contains(CLASS_FILE); + } else { + return size == 1 && + (scopes.contains(JAVA_FILE) + || scopes.contains(CLASS_FILE) + || scopes.contains(RESOURCE_FILE) + || scopes.contains(PROGUARD_FILE) + || scopes.contains(PROPERTY_FILE) + || scopes.contains(GRADLE_FILE) + || scopes.contains(MANIFEST)); + } + } + + /** + * Returns the intersection of two scope sets + * + * @param scope1 the first set to intersect + * @param scope2 the second set to intersect + * @return the intersection of the two sets + */ + @NonNull + public static EnumSet intersect( + @NonNull EnumSet scope1, + @NonNull EnumSet scope2) { + EnumSet scope = EnumSet.copyOf(scope1); + scope.retainAll(scope2); + + return scope; + } + + /** + * Infers a suitable scope to use from the given projects to be analyzed + * @param projects the projects to find a suitable scope for + * @return the scope to use + */ + @NonNull + public static EnumSet infer(@NonNull Collection projects) { + // Infer the scope + EnumSet scope = EnumSet.noneOf(Scope.class); + for (Project project : projects) { + List subset = project.getSubset(); + if (subset != null) { + for (File file : subset) { + String name = file.getName(); + if (name.equals(ANDROID_MANIFEST_XML)) { + scope.add(MANIFEST); + } else if (name.endsWith(DOT_XML)) { + scope.add(RESOURCE_FILE); + } else if (name.endsWith(DOT_JAVA)) { + scope.add(JAVA_FILE); + } else if (name.endsWith(DOT_CLASS)) { + scope.add(CLASS_FILE); + } else if (name.endsWith(DOT_GRADLE)) { + scope.add(GRADLE_FILE); + } else if (name.equals(OLD_PROGUARD_FILE) + || name.equals(FN_PROJECT_PROGUARD_FILE)) { + scope.add(PROGUARD_FILE); + } else if (name.endsWith(DOT_PROPERTIES)) { + scope.add(PROPERTY_FILE); + } else if (name.endsWith(DOT_PNG)) { + scope.add(BINARY_RESOURCE_FILE); + } else if (name.equals(RES_FOLDER) + || file.getParent().equals(RES_FOLDER)) { + scope.add(ALL_RESOURCE_FILES); + scope.add(RESOURCE_FILE); + scope.add(BINARY_RESOURCE_FILE); + scope.add(RESOURCE_FOLDER); + } + } + } else { + // Specified a full project: just use the full project scope + scope = Scope.ALL; + break; + } + } + + return scope; + } + + /** All scopes: running lint on a project will check these scopes */ + public static final EnumSet ALL = EnumSet.allOf(Scope.class); + /** Scope-set used for detectors which are affected by a single resource file */ + public static final EnumSet RESOURCE_FILE_SCOPE = EnumSet.of(RESOURCE_FILE); + /** Scope-set used for detectors which are affected by a single resource folder */ + public static final EnumSet RESOURCE_FOLDER_SCOPE = EnumSet.of(RESOURCE_FOLDER); + /** Scope-set used for detectors which scan all resources */ + public static final EnumSet ALL_RESOURCES_SCOPE = EnumSet.of(ALL_RESOURCE_FILES); + /** Scope-set used for detectors which are affected by a single Java source file */ + public static final EnumSet JAVA_FILE_SCOPE = EnumSet.of(JAVA_FILE); + /** Scope-set used for detectors which are affected by a single Java class file */ + public static final EnumSet CLASS_FILE_SCOPE = EnumSet.of(CLASS_FILE); + /** Scope-set used for detectors which are affected by a single Gradle build file */ + public static final EnumSet GRADLE_SCOPE = EnumSet.of(GRADLE_FILE); + /** Scope-set used for detectors which are affected by the manifest only */ + public static final EnumSet MANIFEST_SCOPE = EnumSet.of(MANIFEST); + /** Scope-set used for detectors which correspond to some other context */ + public static final EnumSet OTHER_SCOPE = EnumSet.of(OTHER); + /** Scope-set used for detectors which are affected by a single ProGuard class file */ + public static final EnumSet PROGUARD_SCOPE = EnumSet.of(PROGUARD_FILE); + /** Scope-set used for detectors which correspond to property files */ + public static final EnumSet PROPERTY_SCOPE = EnumSet.of(PROPERTY_FILE); + /** Resource XML files and manifest files */ + public static final EnumSet MANIFEST_AND_RESOURCE_SCOPE = + EnumSet.of(Scope.MANIFEST, Scope.RESOURCE_FILE); + /** Scope-set used for detectors which are affected by single XML and Java source files */ + public static final EnumSet JAVA_AND_RESOURCE_FILES = + EnumSet.of(RESOURCE_FILE, JAVA_FILE); + /** Scope-set used for analyzing individual class files and all resource files */ + public static final EnumSet CLASS_AND_ALL_RESOURCE_FILES = + EnumSet.of(ALL_RESOURCE_FILES, CLASS_FILE); + /** Scope-set used for analyzing all class files, including those in libraries */ + public static final EnumSet ALL_CLASSES_AND_LIBRARIES = + EnumSet.of(Scope.ALL_CLASS_FILES, Scope.JAVA_LIBRARIES); + /** Scope-set used for detectors which are affected by Java libraries */ + public static final EnumSet JAVA_LIBRARY_SCOPE = EnumSet.of(JAVA_LIBRARIES); + /** Scope-set used for detectors which are affected by a single binary resource file */ + public static final EnumSet BINARY_RESOURCE_FILE_SCOPE = + EnumSet.of(BINARY_RESOURCE_FILE); +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Severity.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Severity.java new file mode 100644 index 00000000000..e0f90f0857c --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Severity.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2011 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.detector.api; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.google.common.annotations.Beta; + +/** + * Severity of an issue found by lint + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public enum Severity { + /** + * Fatal: Use sparingly because a warning marked as fatal will be + * considered critical and will abort Export APK etc in ADT + */ + @NonNull + FATAL("Fatal"), + + /** + * Errors: The issue is known to be a real error that must be addressed. + */ + @NonNull + ERROR("Error"), + + /** + * Warning: Probably a problem. + */ + @NonNull + WARNING("Warning"), + + /** + * Information only: Might not be a problem, but the check has found + * something interesting to say about the code. + */ + @NonNull + INFORMATIONAL("Information"), + + /** + * Ignore: The user doesn't want to see this issue + */ + @NonNull + IGNORE("Ignore"); + + @NonNull + private final String mDisplay; + + Severity(@NonNull String display) { + mDisplay = display; + } + + /** + * Returns a description of this severity suitable for display to the user + * + * @return a description of the severity + */ + @NonNull + public String getDescription() { + return mDisplay; + } + + /** Returns the name of this severity */ + @NonNull + public String getName() { + return name(); + } + + /** + * Looks up the severity corresponding to a given named severity. The severity + * string should be one returned by {@link #toString()} + * + * @param name the name to look up + * @return the corresponding severity, or null if it is not a valid severity name + */ + @Nullable + public static Severity fromName(@NonNull String name) { + for (Severity severity : values()) { + if (severity.name().equalsIgnoreCase(name)) { + return severity; + } + } + + return null; + } +} \ No newline at end of file diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Speed.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Speed.java new file mode 100644 index 00000000000..4ef1eb1702e --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/Speed.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2011 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.detector.api; + +import com.android.annotations.NonNull; +import com.google.common.annotations.Beta; + +/** + * Enum which describes the different computation speeds of various detectors + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public enum Speed { + /** The detector can run very quickly */ + FAST("Fast"), + + /** The detector runs reasonably fast */ + NORMAL("Normal"), + + /** The detector might take a long time to run */ + SLOW("Slow"), + + /** The detector might take a huge amount of time to run */ + REALLY_SLOW("Really Slow"); + + private final String mDisplayName; + + Speed(@NonNull String displayName) { + mDisplayName = displayName; + } + + /** + * Returns the user-visible description of the speed of the given + * detector + * + * @return the description of the speed to display to the user + */ + @NonNull + public String getDisplayName() { + return mDisplayName; + } +} \ No newline at end of file diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/TextFormat.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/TextFormat.java new file mode 100644 index 00000000000..ed1bd3ca90d --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/TextFormat.java @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2014 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.detector.api; + +import com.android.annotations.NonNull; +import com.android.utils.SdkUtils; +import com.android.utils.XmlUtils; + +/** + * Lint error message, issue explanations and location descriptions + * are described in a {@link #RAW} format which looks similar to text + * but which can contain bold, symbols and links. These issues can + * also be converted to plain text and to HTML markup, using the + * {@link #convertTo(String, TextFormat)} method. + * + * @see Issue#getDescription(TextFormat) + * @see Issue#getExplanation(TextFormat) + * @see Issue#getBriefDescription(TextFormat) + */ +public enum TextFormat { + /** + * Raw output format which is similar to text but allows some markup: + *

    + *
  • HTTP urls (http://...) + *
  • Sentences immediately surrounded by * will be shown as bold. + *
  • Sentences immediately surrounded by ` will be shown using monospace + * fonts + *
+ * Furthermore, newlines are converted to br's when converting newlines. + * Note: It does not insert {@code } tags around the fragment for HTML output. + *

+ * TODO: Consider switching to the restructured text format - + * http://docutils.sourceforge.net/docs/user/rst/quickstart.html + */ + RAW, + + /** + * Plain text output + */ + TEXT, + + /** + * HTML formatted output (note: does not include surrounding {@code } tags) + */ + HTML; + + /** + * Converts the given text to HTML + * + * @param text the text to format + * @return the corresponding text formatted as HTML + */ + @NonNull + public String toHtml(@NonNull String text) { + return convertTo(text, HTML); + } + + /** + * Converts the given text to plain text + * + * @param text the tetx to format + * @return the corresponding text formatted as HTML + */ + @NonNull + public String toText(@NonNull String text) { + return convertTo(text, TEXT); + } + + /** + * Converts the given message to the given format. Note that some + * conversions are lossy; e.g. once converting away from the raw format + * (which contains all the markup) you can't convert back to it. + * Note that you can convert to the format it's already in; that just + * returns the same string. + * + * @param message the message to convert + * @param to the format to convert to + * @return a converted message + */ + public String convertTo(@NonNull String message, @NonNull TextFormat to) { + if (this == to) { + return message; + } + switch (this) { + case RAW: { + switch (to) { + case RAW: + return message; + case TEXT: + case HTML: + return to.fromRaw(message); + } + } + case TEXT: { + switch (to) { + case TEXT: + case RAW: + return message; + case HTML: + return XmlUtils.toXmlTextValue(message); + } + } + case HTML: { + switch (to) { + case HTML: + return message; + case RAW: + case TEXT: { + return to.fromHtml(message); + + } + } + } + } + return message; + } + + /** Converts to this output format from the given HTML-format text */ + @NonNull + private String fromHtml(@NonNull String html) { + assert this == RAW || this == TEXT : this; + + // Drop all tags; replace all entities, insert newlines + // (this won't do wrapping) + StringBuilder sb = new StringBuilder(html.length()); + for (int i = 0, n = html.length(); i < n; i++) { + char c = html.charAt(i); + if (c == '<') { + // Scan forward to the end + if (html.startsWith("
", i) || + html.startsWith("
", i) || + html.startsWith("
", i) || + html.startsWith("
", i)) { + sb.append('\n'); + } else if (html.startsWith("", i)); + } + i = html.indexOf('>', i); + } else if (c == '&') { + int end = html.indexOf(';', i); + if (end > i) { + String entity = html.substring(i, end + 1); + sb.append(XmlUtils.fromXmlAttributeValue(entity)); + i = end; + } else { + sb.append(c); + } + } else if (c == '\n') { + sb.append(' '); + } else { + sb.append(c); + } + } + + // Collapse repeated spaces + String s = sb.toString(); + sb.setLength(0); + boolean wasSpace = false; + for (int i = 0, n = s.length(); i < n; i++) { + char c = s.charAt(i); + if (c == '\t') { // we keep newlines; came from
's + c = ' '; + } + boolean isSpace = c == ' '; + if (!isSpace || !wasSpace) { + wasSpace = isSpace; + sb.append(c); + } + } + s = sb.toString(); + + // Line-wrap + s = SdkUtils.wrap(s, 60, null); + + return s; + } + + private static final String HTTP_PREFIX = "http://"; //$NON-NLS-1$ + + /** Converts to this output format from the given raw-format text */ + @NonNull + private String fromRaw(@NonNull String text) { + assert this == HTML || this == TEXT : this; + StringBuilder sb = new StringBuilder(3 * text.length() / 2); + boolean html = this == HTML; + + char prev = 0; + int flushIndex = 0; + int n = text.length(); + for (int i = 0; i < n; i++) { + char c = text.charAt(i); + if ((c == '*' || c == '`' && i < n - 1)) { + // Scout ahead for range end + if (!Character.isLetterOrDigit(prev) + && !Character.isWhitespace(text.charAt(i + 1))) { + // Found * or ` immediately before a letter, and not in the middle of a word + // Find end + int end = text.indexOf(c, i + 1); + if (end != -1 && (end == n - 1 || !Character.isLetter(text.charAt(end + 1)))) { + if (i > flushIndex) { + appendEscapedText(sb, text, html, flushIndex, i); + } + if (html) { + String tag = c == '*' ? "b" : "code"; //$NON-NLS-1$ //$NON-NLS-2$ + sb.append('<').append(tag).append('>'); + appendEscapedText(sb, text, html, i + 1, end); + sb.append('<').append('/').append(tag).append('>'); + } else { + appendEscapedText(sb, text, html, i + 1, end); + } + flushIndex = end + 1; + i = flushIndex - 1; // -1: account for the i++ in the loop + } + } + } else if (html && c == 'h' && i < n - 1 && text.charAt(i + 1) == 't' + && text.startsWith(HTTP_PREFIX, i) && !Character.isLetterOrDigit(prev)) { + // Find url end + int end = i + HTTP_PREFIX.length(); + while (end < n) { + char d = text.charAt(end); + if (Character.isWhitespace(d)) { + break; + } + end++; + } + char last = text.charAt(end - 1); + if (last == '.' || last == ')' || last == '!') { + end--; + } + if (end > i + HTTP_PREFIX.length()) { + if (i > flushIndex) { + appendEscapedText(sb, text, html, flushIndex, i); + } + + String url = text.substring(i, end); + sb.append("'); + sb.append(url); + sb.append(""); //$NON-NLS-1$ + + flushIndex = end; + i = flushIndex - 1; // -1: account for the i++ in the loop + } + } + prev = c; + } + + if (flushIndex < n) { + appendEscapedText(sb, text, html, flushIndex, n); + } + + return sb.toString(); + } + + private static void appendEscapedText(@NonNull StringBuilder sb, @NonNull String text, + boolean html, int start, int end) { + if (html) { + for (int i = start; i < end; i++) { + char c = text.charAt(i); + if (c == '<') { + sb.append("<"); //$NON-NLS-1$ + } else if (c == '&') { + sb.append("&"); //$NON-NLS-1$ + } else if (c == '\n') { + sb.append("
\n"); + } else { + if (c > 255) { + sb.append("&#"); //$NON-NLS-1$ + sb.append(Integer.toString(c)); + sb.append(';'); + } else if (c == '\u00a0') { + sb.append(" "); //$NON-NLS-1$ + } else { + sb.append(c); + } + } + } + } else { + for (int i = start; i < end; i++) { + char c = text.charAt(i); + sb.append(c); + } + } + } +} diff --git a/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/XmlContext.java b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/XmlContext.java new file mode 100644 index 00000000000..891eced0f72 --- /dev/null +++ b/plugins/lint/lint-api/src/com/android/tools/klint/detector/api/XmlContext.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2011 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.detector.api; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.resources.ResourceFolderType; +import com.android.tools.lint.client.api.LintDriver; +import com.android.tools.lint.client.api.XmlParser; +import com.google.common.annotations.Beta; + +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import java.io.File; + +/** + * A {@link Context} used when checking XML files. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + */ +@Beta +public class XmlContext extends ResourceContext { + static final String SUPPRESS_COMMENT_PREFIX = "