1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.databinding.tool.processing;
18 
19 import android.databinding.tool.processing.scopes.FileScopeProvider;
20 import android.databinding.tool.processing.scopes.LocationScopeProvider;
21 import android.databinding.tool.processing.scopes.ScopeProvider;
22 import android.databinding.tool.store.Location;
23 import android.databinding.tool.util.Preconditions;
24 
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.HashSet;
28 import java.util.List;
29 
30 /**
31  * Utility class to keep track of "logical" stack traces, which we can use to print better error
32  * reports.
33  */
34 public class Scope {
35 
36     private static ThreadLocal<ScopeEntry> sScopeItems = new ThreadLocal<ScopeEntry>();
37     static List<ScopedException> sDeferredExceptions = new ArrayList<>();
38 
enter(final Location location)39     public static void enter(final Location location) {
40         enter(new LocationScopeProvider() {
41             @Override
42             public List<Location> provideScopeLocation() {
43                 return Arrays.asList(location);
44             }
45         });
46     }
enter(ScopeProvider scopeProvider)47     public static void enter(ScopeProvider scopeProvider) {
48         ScopeEntry peek = sScopeItems.get();
49         ScopeEntry entry = new ScopeEntry(scopeProvider, peek);
50         sScopeItems.set(entry);
51     }
52 
exit()53     public static void exit() {
54         ScopeEntry entry = sScopeItems.get();
55         Preconditions.checkNotNull(entry, "Inconsistent scope exit");
56         sScopeItems.set(entry.mParent);
57     }
58 
defer(ScopedException exception)59     public static void defer(ScopedException exception) {
60         sDeferredExceptions.add(exception);
61     }
62 
registerErrorInternal(String msg, int scopeIndex, ScopeProvider... scopeProviders)63     private static void registerErrorInternal(String msg, int scopeIndex,
64             ScopeProvider... scopeProviders) {
65         if (scopeProviders == null || scopeProviders.length <= scopeIndex) {
66             defer(new ScopedException(msg));
67         } else if (scopeProviders[scopeIndex] == null) {
68             registerErrorInternal(msg, scopeIndex + 1, scopeProviders);
69         } else {
70             try {
71                 Scope.enter(scopeProviders[scopeIndex]);
72                 registerErrorInternal(msg, scopeIndex + 1, scopeProviders);
73             } finally {
74                 Scope.exit();
75             }
76         }
77     }
78 
79     /**
80      * Convenience method to add an error in a known list of scopes, w/o adding try catch flows.
81      * <p>
82      * This code actually starts entering the given scopes 1 by 1, starting from 0. When list is
83      * consumed, it creates the exception and defers if possible then exits from the provided
84      * scopes.
85      * <p>
86      * Note that these scopes are added on top of the already existing scopes.
87      *
88      * @param msg The exception message
89      * @param scopeProviders The list of additional scope providers to enter. Null scopes are
90      *                       automatically ignored.
91      */
registerError(String msg, ScopeProvider... scopeProviders)92     public static void registerError(String msg, ScopeProvider... scopeProviders) {
93         registerErrorInternal(msg, 0, scopeProviders);
94     }
95 
assertNoError()96     public static void assertNoError() {
97         if (sDeferredExceptions.isEmpty()) {
98             return;
99         }
100         StringBuilder sb = new StringBuilder();
101         HashSet<String> messages = new HashSet<String>();
102         for (ScopedException ex : sDeferredExceptions) {
103             final String message = ex.getMessage();
104             if (!messages.contains(message)) {
105                 sb.append(message).append("\n");
106                 messages.add(message);
107             }
108         }
109         throw new RuntimeException("Found data binding errors.\n" + sb.toString());
110     }
111 
produceScopeLog()112     static String produceScopeLog() {
113         StringBuilder sb = new StringBuilder();
114         sb.append("full scope log\n");
115         ScopeEntry top = sScopeItems.get();
116         while (top != null) {
117             ScopeProvider provider = top.mProvider;
118             sb.append("---").append(provider).append("\n");
119             if (provider instanceof FileScopeProvider) {
120                 sb.append("file:").append(((FileScopeProvider) provider).provideScopeFilePath())
121                         .append("\n");
122             }
123             if (provider instanceof LocationScopeProvider) {
124                 LocationScopeProvider loc = (LocationScopeProvider) provider;
125                 sb.append("loc:");
126                 List<Location> locations = loc.provideScopeLocation();
127                 if (locations == null) {
128                     sb.append("null\n");
129                 } else {
130                     for (Location location : locations) {
131                         sb.append(location).append("\n");
132                     }
133                 }
134             }
135             top = top.mParent;
136         }
137         sb.append("---\n");
138         return sb.toString();
139     }
140 
createReport()141     static ScopedErrorReport createReport() {
142         ScopeEntry top = sScopeItems.get();
143         String filePath = null;
144         List<Location> locations = null;
145         while (top != null && (filePath == null || locations == null)) {
146             ScopeProvider provider = top.mProvider;
147             if (locations == null && provider instanceof LocationScopeProvider) {
148                 locations = findAbsoluteLocationFrom(top, (LocationScopeProvider) provider);
149             }
150             if (filePath == null && provider instanceof FileScopeProvider) {
151                 filePath = ((FileScopeProvider) provider).provideScopeFilePath();
152             }
153             top = top.mParent;
154         }
155         return new ScopedErrorReport(filePath, locations);
156     }
157 
findAbsoluteLocationFrom(ScopeEntry entry, LocationScopeProvider top)158     private static List<Location> findAbsoluteLocationFrom(ScopeEntry entry,
159             LocationScopeProvider top) {
160         List<Location> locations = top.provideScopeLocation();
161         if (locations == null || locations.isEmpty()) {
162             return null;
163         }
164         if (locations.size() == 1) {
165             return Arrays.asList(locations.get(0).toAbsoluteLocation());
166         }
167         // We have more than 1 location. Depending on the scope, we may or may not want all of them
168         List<Location> chosen = new ArrayList<>();
169         for (Location location : locations) {
170             Location absLocation = location.toAbsoluteLocation();
171             if (validatedContained(entry.mParent, absLocation)) {
172                 chosen.add(absLocation);
173             }
174         }
175         return chosen.isEmpty() ? locations : chosen;
176     }
177 
validatedContained(ScopeEntry parent, Location absLocation)178     private static boolean validatedContained(ScopeEntry parent, Location absLocation) {
179         if (parent == null) {
180             return true;
181         }
182         ScopeProvider provider = parent.mProvider;
183         if (!(provider instanceof LocationScopeProvider)) {
184             return validatedContained(parent.mParent, absLocation);
185         }
186         List<Location> absoluteParents = findAbsoluteLocationFrom(parent,
187                 (LocationScopeProvider) provider);
188         for (Location location : absoluteParents) {
189             if (location.contains(absLocation)) {
190                 return true;
191             }
192         }
193         return false;
194     }
195 
196     private static class ScopeEntry {
197 
198         ScopeProvider mProvider;
199 
200         ScopeEntry mParent;
201 
ScopeEntry(ScopeProvider scopeProvider, ScopeEntry parent)202         public ScopeEntry(ScopeProvider scopeProvider, ScopeEntry parent) {
203             mProvider = scopeProvider;
204             mParent = parent;
205         }
206     }
207 }