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 }