View Javadoc
1   /*
2    * Copyright (c) 2002-2026 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.VarScope;
29  import org.htmlunit.corejs.javascript.debug.DebuggableScript;
30  import org.htmlunit.javascript.host.event.Event;
31  
32  /**
33   * <p>
34   * HtmlUnit's implementation of the {@link org.htmlunit.corejs.javascript.debug.DebugFrame} interface,
35   * which logs stack entries as well as exceptions. All logging is done at the <code>TRACE</code> level. This class does
36   * a fairly good job of guessing names for anonymous functions when they are referenced by name from an existing
37   * object. See <a href="http://www.mozilla.org/rhino/rhino15R4-debugger.html">the Rhino documentation</a> or
38   * <a href="http://lxr.mozilla.org/mozilla/source/js/rhino/src/org/mozilla/javascript/debug/DebugFrame.java">the
39   * interface source code</a> for more information on the
40   * {@link org.htmlunit.corejs.javascript.debug.DebugFrame} interface and its uses.
41   * </p>
42   *
43   * <p>
44   * Please note that this class is intended mainly to aid in the debugging and development of
45   * HtmlUnit itself, rather than the debugging and development of web applications.
46   * </p>
47   *
48   * @see DebuggerImpl
49   *
50   * @author Daniel Gredler
51   * @author Marc Guillemot
52   * @author Sven Strickroth
53   * @author Ronald Brill
54   */
55  public class DebugFrameImpl extends DebugFrameAdapter {
56  
57      private static final Log LOG = LogFactory.getLog(DebugFrameImpl.class);
58  
59      private static final String KEY_LAST_LINE = "DebugFrameImpl#line";
60      private static final String KEY_LAST_SOURCE = "DebugFrameImpl#source";
61  
62      private final DebuggableScript functionOrScript_;
63  
64      /**
65       * Creates a new debug frame.
66       *
67       * @param functionOrScript the function or script to which this frame corresponds
68       */
69      public DebugFrameImpl(final DebuggableScript functionOrScript) {
70          super();
71          functionOrScript_ = functionOrScript;
72      }
73  
74      /**
75       * {@inheritDoc}
76       */
77      @Override
78      public void onEnter(final Context cx, final VarScope activation, final Scriptable thisObj, final Object[] args) {
79          if (LOG.isTraceEnabled()) {
80              final StringBuilder sb = new StringBuilder();
81  
82              final String line = getFirstLine(cx);
83              final String source = getSourceName(cx);
84              sb.append(source).append(':').append(line).append(' ');
85  
86              VarScope parent = activation.getParentScope();
87              while (parent != null) {
88                  sb.append("   ");
89                  parent = parent.getParentScope();
90              }
91              final String functionName = getFunctionName(thisObj);
92              sb.append(functionName).append('(');
93              final int nbParams = functionOrScript_.getParamCount();
94              for (int i = 0; i < nbParams; i++) {
95                  final String argAsString;
96                  if (i < args.length) {
97                      argAsString = stringValue(args[i]);
98                  }
99                  else {
100                     argAsString = "undefined";
101                 }
102                 sb.append(getParamName(i)).append(": ").append(argAsString);
103                 if (i < nbParams - 1) {
104                     sb.append(", ");
105                 }
106             }
107             sb.append(')');
108 
109             LOG.trace(sb);
110         }
111     }
112 
113     private static String stringValue(final Object arg) {
114         if (arg instanceof NativeFunction function) {
115             // Don't return the string value of the function, because it's usually
116             // multiple lines of content and messes up the log.
117             final String name = StringUtils.defaultIfEmpty(function.getFunctionName(), "anonymous");
118             return "[function " + name + "]";
119         }
120         else if (arg instanceof IdFunctionObject object) {
121             return "[function " + object.getFunctionName() + "]";
122         }
123         else if (arg instanceof Function) {
124             return "[function anonymous]";
125         }
126         String asString;
127         try {
128             // try to get the js representation
129             asString = JavaScriptEngine.toString(arg);
130             if (arg instanceof Event event) {
131                 asString += "<" + event.getType() + ">";
132             }
133         }
134         catch (final Throwable e) {
135             // seems to be a bug (many bugs) in rhino (TODO: investigate it)
136             asString = String.valueOf(arg);
137         }
138         return asString;
139     }
140 
141     /**
142      * {@inheritDoc}
143      */
144     @Override
145     public void onExceptionThrown(final Context cx, final Throwable t) {
146         if (LOG.isTraceEnabled()) {
147             if (t instanceof JavaScriptException e) {
148                 LOG.trace(getSourceName(cx) + ":" + getFirstLine(cx)
149                     + " Exception thrown: " + e.details());
150             }
151             else if (t instanceof EcmaError e) {
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 s) {
201                         if (obj instanceof ScriptableObject object) {
202                             Object o = object.getGetterOrSetter(s, 0, thisObj, false);
203                             if (o == null) {
204                                 o = object.getGetterOrSetter(s, 0, thisObj, true);
205                                 if (o != null) {
206                                     return "__defineSetter__ " + s;
207                                 }
208                             }
209                             else {
210                                 return "__defineGetter__ " + s;
211                             }
212                         }
213                         final Object o;
214                         try {
215                             // within a try block as this sometimes throws (not sure why)
216                             o = obj.get(s, obj);
217                         }
218                         catch (final Exception e) {
219                             return "[anonymous]";
220                         }
221                         if (o instanceof NativeFunction f) {
222                             if (f.getDebuggableView() == functionOrScript_) {
223                                 return s;
224                             }
225                         }
226                     }
227                 }
228                 obj = obj.getPrototype();
229             }
230             // Unable to intuit a name -- doh!
231             return "[anonymous]";
232         }
233         // Just a script -- no function name.
234         return "[script]";
235     }
236 
237     /**
238      * Returns the name of the parameter at the specified index, or <code>???</code> if there is no
239      * corresponding name.
240      *
241      * @param index the index of the parameter whose name is to be returned
242      * @return the name of the parameter at the specified index, or <code>???</code> if there is no corresponding name
243      */
244     private String getParamName(final int index) {
245         if (index >= 0 && functionOrScript_.getParamCount() > index) {
246             return functionOrScript_.getParamOrVarName(index);
247         }
248         return "???";
249     }
250 
251     /**
252      * Returns the name of this frame's source.
253      *
254      * @return the name of this frame's source
255      */
256     private static String getSourceName(final Context cx) {
257         String source = (String) cx.getThreadLocal(KEY_LAST_SOURCE);
258         if (source == null) {
259             return "unknown";
260         }
261         // only the file name is interesting the rest of the url is mostly noise
262         source = StringUtils.substringAfterLast(source, "/");
263         // embedded scripts have something like "foo.html from (3, 10) to (10, 13)"
264         source = org.htmlunit.util.StringUtils.substringBefore(source, " ");
265         return source;
266     }
267 
268     /**
269      * Returns the line number of the first line in this frame's function or script, or <code>???</code>
270      * if it cannot be determined. This is necessary because the line numbers provided by Rhino are unordered.
271      *
272      * @return the line number of the first line in this frame's function or script, or <code>???</code>
273      *         if it cannot be determined
274      */
275     private static String getFirstLine(final Context cx) {
276         final Object line = cx.getThreadLocal(KEY_LAST_LINE);
277         final String result;
278         if (line == null) {
279             result = "??";
280         }
281         else {
282             result = String.valueOf(line);
283         }
284         return StringUtils.leftPad(result, 5);
285     }
286 }