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 }