View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
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    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.javascript;
16  
17  import org.apache.commons.lang3.StringUtils;
18  import org.apache.commons.logging.Log;
19  import org.apache.commons.logging.LogFactory;
20  import org.htmlunit.corejs.javascript.Context;
21  import org.htmlunit.corejs.javascript.EcmaError;
22  import org.htmlunit.corejs.javascript.Function;
23  import org.htmlunit.corejs.javascript.IdFunctionObject;
24  import org.htmlunit.corejs.javascript.JavaScriptException;
25  import org.htmlunit.corejs.javascript.NativeFunction;
26  import org.htmlunit.corejs.javascript.Scriptable;
27  import org.htmlunit.corejs.javascript.ScriptableObject;
28  import org.htmlunit.corejs.javascript.debug.DebuggableScript;
29  import org.htmlunit.javascript.host.event.Event;
30  
31  /**
32   * <p>
33   * HtmlUnit's implementation of the {@link org.htmlunit.corejs.javascript.debug.DebugFrame} interface,
34   * which logs stack entries as well as exceptions. All logging is done at the <code>TRACE</code> level. This class does
35   * a fairly good job of guessing names for anonymous functions when they are referenced by name from an existing
36   * object. See <a href="http://www.mozilla.org/rhino/rhino15R4-debugger.html">the Rhino documentation</a> or
37   * <a href="http://lxr.mozilla.org/mozilla/source/js/rhino/src/org/mozilla/javascript/debug/DebugFrame.java">the
38   * interface source code</a> for more information on the
39   * {@link org.htmlunit.corejs.javascript.debug.DebugFrame} interface and its uses.
40   * </p>
41   *
42   * <p>
43   * Please note that this class is intended mainly to aid in the debugging and development of
44   * HtmlUnit itself, rather than the debugging and development of web applications.
45   * </p>
46   *
47   * @author Daniel Gredler
48   * @author Marc Guillemot
49   * @author Sven Strickroth
50   *
51   * @see DebuggerImpl
52   */
53  public class DebugFrameImpl extends DebugFrameAdapter {
54  
55      private static final Log LOG = LogFactory.getLog(DebugFrameImpl.class);
56  
57      private static final String KEY_LAST_LINE = "DebugFrameImpl#line";
58      private static final String KEY_LAST_SOURCE = "DebugFrameImpl#source";
59  
60      private final DebuggableScript functionOrScript_;
61  
62      /**
63       * Creates a new debug frame.
64       *
65       * @param functionOrScript the function or script to which this frame corresponds
66       */
67      public DebugFrameImpl(final DebuggableScript functionOrScript) {
68          super();
69          functionOrScript_ = functionOrScript;
70      }
71  
72      /**
73       * {@inheritDoc}
74       */
75      @Override
76      public void onEnter(final Context cx, final Scriptable activation, final Scriptable thisObj, final Object[] args) {
77          if (LOG.isTraceEnabled()) {
78              final StringBuilder sb = new StringBuilder();
79  
80              final String line = getFirstLine(cx);
81              final String source = getSourceName(cx);
82              sb.append(source).append(':').append(line).append(' ');
83  
84              Scriptable parent = activation.getParentScope();
85              while (parent != null) {
86                  sb.append("   ");
87                  parent = parent.getParentScope();
88              }
89              final String functionName = getFunctionName(thisObj);
90              sb.append(functionName).append('(');
91              final int nbParams = functionOrScript_.getParamCount();
92              for (int i = 0; i < nbParams; i++) {
93                  final String argAsString;
94                  if (i < args.length) {
95                      argAsString = stringValue(args[i]);
96                  }
97                  else {
98                      argAsString = "undefined";
99                  }
100                 sb.append(getParamName(i)).append(": ").append(argAsString);
101                 if (i < nbParams - 1) {
102                     sb.append(", ");
103                 }
104             }
105             sb.append(')');
106 
107             LOG.trace(sb);
108         }
109     }
110 
111     private static String stringValue(final Object arg) {
112         if (arg instanceof NativeFunction) {
113             // Don't return the string value of the function, because it's usually
114             // multiple lines of content and messes up the log.
115             final String name = StringUtils.defaultIfEmpty(((NativeFunction) arg).getFunctionName(), "anonymous");
116             return "[function " + name + "]";
117         }
118         else if (arg instanceof IdFunctionObject) {
119             return "[function " + ((IdFunctionObject) arg).getFunctionName() + "]";
120         }
121         else if (arg instanceof Function) {
122             return "[function anonymous]";
123         }
124         String asString;
125         try {
126             // try to get the js representation
127             asString = JavaScriptEngine.toString(arg);
128             if (arg instanceof Event) {
129                 asString += "<" + ((Event) arg).getType() + ">";
130             }
131         }
132         catch (final Throwable e) {
133             // seems to be a bug (many bugs) in rhino (TODO: investigate it)
134             asString = String.valueOf(arg);
135         }
136         return asString;
137     }
138 
139     /**
140      * {@inheritDoc}
141      */
142     @Override
143     public void onExceptionThrown(final Context cx, final Throwable t) {
144         if (LOG.isTraceEnabled()) {
145             if (t instanceof JavaScriptException) {
146                 final JavaScriptException e = (JavaScriptException) t;
147                 LOG.trace(getSourceName(cx) + ":" + getFirstLine(cx)
148                     + " Exception thrown: " + e.details());
149             }
150             else if (t instanceof EcmaError) {
151                 final EcmaError e = (EcmaError) t;
152                 LOG.trace(getSourceName(cx) + ":" + getFirstLine(cx)
153                     + " Exception thrown: " + e.details());
154             }
155             else {
156                 LOG.trace(getSourceName(cx) + ":" + getFirstLine(cx) + " Exception thrown: " + t.getCause());
157             }
158         }
159     }
160 
161     /**
162      * {@inheritDoc}
163      */
164     @Override
165     public void onLineChange(final Context cx, final int lineNumber) {
166         cx.putThreadLocal(KEY_LAST_LINE, lineNumber);
167         cx.putThreadLocal(KEY_LAST_SOURCE, functionOrScript_.getSourceName());
168     }
169 
170     /**
171      * Returns the name of the function corresponding to this frame, if it is a function and it has
172      * a name. If the function does not have a name, this method will try to return the name under
173      * which it was referenced. See <a
174      * href="http://www.digital-web.com/articles/scope_in_javascript/">this page</a> for a good
175      * explanation of how the <code>thisObj</code> plays into this guess.
176      *
177      * @param thisObj the object via which the function was referenced, used to try to guess a
178      *        function name if the function is anonymous
179      * @return the name of the function corresponding to this frame
180      */
181     private String getFunctionName(final Scriptable thisObj) {
182         if (functionOrScript_.isFunction()) {
183             final String name = functionOrScript_.getFunctionName();
184             if (name != null && !name.isEmpty()) {
185                 // A named function -- we can just return the name.
186                 return name;
187             }
188             // An anonymous function -- try to figure out how it was referenced.
189             // For example, someone may have set foo.prototype.bar = function() { ... };
190             // And then called fooInstance.bar() -- in which case it's "named" bar.
191 
192             // on our HtmlUnitScriptable we need to avoid looking at the properties we have defined => TODO: improve it
193             if (thisObj instanceof HtmlUnitScriptable) {
194                 return "[anonymous]";
195             }
196 
197             Scriptable obj = thisObj;
198             while (obj != null) {
199                 for (final Object id : obj.getIds()) {
200                     if (id instanceof String) {
201                         final String s = (String) id;
202                         if (obj instanceof ScriptableObject) {
203                             Object o = ((ScriptableObject) obj).getGetterOrSetter(s, 0, thisObj, false);
204                             if (o == null) {
205                                 o = ((ScriptableObject) obj).getGetterOrSetter(s, 0, thisObj, true);
206                                 if (o != null) {
207                                     return "__defineSetter__ " + s;
208                                 }
209                             }
210                             else {
211                                 return "__defineGetter__ " + s;
212                             }
213                         }
214                         final Object o;
215                         try {
216                             // within a try block as this sometimes throws (not sure why)
217                             o = obj.get(s, obj);
218                         }
219                         catch (final Exception e) {
220                             return "[anonymous]";
221                         }
222                         if (o instanceof NativeFunction) {
223                             final NativeFunction f = (NativeFunction) o;
224                             if (f.getDebuggableView() == functionOrScript_) {
225                                 return s;
226                             }
227                         }
228                     }
229                 }
230                 obj = obj.getPrototype();
231             }
232             // Unable to intuit a name -- doh!
233             return "[anonymous]";
234         }
235         // Just a script -- no function name.
236         return "[script]";
237     }
238 
239     /**
240      * Returns the name of the parameter at the specified index, or <code>???</code> if there is no
241      * corresponding name.
242      *
243      * @param index the index of the parameter whose name is to be returned
244      * @return the name of the parameter at the specified index, or <code>???</code> if there is no corresponding name
245      */
246     private String getParamName(final int index) {
247         if (index >= 0 && functionOrScript_.getParamCount() > index) {
248             return functionOrScript_.getParamOrVarName(index);
249         }
250         return "???";
251     }
252 
253     /**
254      * Returns the name of this frame's source.
255      *
256      * @return the name of this frame's source
257      */
258     private static String getSourceName(final Context cx) {
259         String source = (String) cx.getThreadLocal(KEY_LAST_SOURCE);
260         if (source == null) {
261             return "unknown";
262         }
263         // only the file name is interesting the rest of the url is mostly noise
264         source = StringUtils.substringAfterLast(source, "/");
265         // embedded scripts have something like "foo.html from (3, 10) to (10, 13)"
266         source = StringUtils.substringBefore(source, " ");
267         return source;
268     }
269 
270     /**
271      * Returns the line number of the first line in this frame's function or script, or <code>???</code>
272      * if it cannot be determined. This is necessary because the line numbers provided by Rhino are unordered.
273      *
274      * @return the line number of the first line in this frame's function or script, or <code>???</code>
275      *         if it cannot be determined
276      */
277     private static String getFirstLine(final Context cx) {
278         final Object line = cx.getThreadLocal(KEY_LAST_LINE);
279         final String result;
280         if (line == null) {
281             result = "??";
282         }
283         else {
284             result = String.valueOf(line);
285         }
286         return StringUtils.leftPad(result, 5);
287     }
288 }