Click here to Skip to main content
15,892,737 members
Articles / Desktop Programming / ATL

Function Call Tracing in JScript

Rate me:
Please Sign up or sign in to vote.
4.58/5 (18 votes)
3 Jul 2007CPOL15 min read 53.9K   6.4K   49  
Comprehensive JScript function call tracing without code modification.
/***************************************************************************
*
*	File            : ScriptParser.cpp
*	Author          : K. Skilling
*	Description     : The ScriptParser is responsible for adding the instrumented
*					  code to the script that will eventually be passed to the
*					  scripting engine.  This code calls methods on the __JSD 
*					  object which will at run-time emit the trace statements
*					  to show function entry/exit/arguments, etc.
*
*					  This parser is fairly limited in what it can handle and 
*					  probably fairly inefficient.  It uses the VBScript RegExp
*					  object to test rule mathces - moving to VS2005 would allow 
*					  use of the ATL reg exp classes which might be better.
*
*	Change History
*	---------------
*	KS	04/04/2007	CREATED
*	$Date::  $  $Author::$  Modified
*****************************************************************************/
#include "stdafx.h"
#include "JScriptDebug.h"
#include "ScriptDebugEngine.h"

/////////////////////////////////////////////////////////////////////////////

// Use a local version of output helper
static COutputHelper f_oOutput(L"JSD:");

// Condition compilation gets added to instrumented code
LPCWSTR szCC = L"@set @JSD = true\n\n";

// In proper version this is switchable from ScriptDebugUtility interface
BOOL g_bLogAnonymous = TRUE;

typedef struct xFnDef
{
	int			m_iScope;
	CString		m_sName;
	BOOL		m_bForceTrace;

	xFnDef(LPCWSTR sName,int iScope,BOOL bForceTrace = FALSE) 
			: m_sName(sName),m_iScope(iScope),m_bForceTrace(bForceTrace)
	{
	}

	// Only returns a name if one has been specified to override the
	// anonymous name.  Otherwise "arguments" is returned as this 
	// can be used to get either the real name or "JScript anonymous function" 
	// from the jscript engine.
	CString GetName()
	{
		CString sName(L"arguments");
		if (m_sName.GetLength())
			sName.Format(L"\"%s\"",m_sName);

		return sName;
	}

	// Adds the name of the fn and the optional flag to force the trace output
	// Only need to include the force trace flag if the ret value - which is also
	// optional - is used.  
	CString GetExitArgs(bool bHasReturnValue = false)
	{
		CString sArgs;

		LPCWSTR szForce = m_bForceTrace ? L"true" : L"false";
		if (bHasReturnValue)
			sArgs.Format(L"%s,%s,__x",(LPCWSTR)GetName(),szForce);		// x is return arg.
		else if (m_bForceTrace)
			sArgs.Format(L"%s,%s",(LPCWSTR)GetName(),szForce);
		else
			sArgs.Format(L"%s",(LPCWSTR)GetName());
		return sArgs;
	}

} FNDEF,*PFNDEF;

typedef std::stack<PFNDEF> FNSTACK;
typedef std::list<CString> STRINGLIST;

// Rules & their bit-field positions
const NORULE				= 0;	
const LINECOMMENT			= 1;	const bfLINECOMMENT			= 0x0001;
const BLOCKCOMMENT			= 2;	const bfBLOCKCOMMENT		= 0x0002;
const FUNCTION				= 3;	const bfFUNCTION			= 0x0004;
const RETURN_EMPTY			= 4;	const bfRETURN_EMPTY		= 0x0008;
const RETURN				= 5;	const bfRETURN				= 0x0010;
const RETURN_FUNCTION		= 6;	const bfRETURN_FUNCTION		= 0x0020;
const SCOPE_START			= 7;	const bfSCOPE_START			= 0x0040;
const SCOPE_END				= 8;	const bfSCOPE_END			= 0x0080;
const LITERAL				= 9;	const bfLITERAL				= 0x0100;
const ENDLINECOMMENT		= 10;	const bfENDLINECOMMENT		= 0x0200;
const ENDCOMMENT			= 11;	const bfENDCOMMENT			= 0x0400;
const RETURN_FN_SCOPE_END	= 12;	const bfRETURN_FN_SCOPE_END	= 0x0800;
const RETURN_SCOPE_START	= 13;	const bfRETURN_SCOPE_START	= 0x1000;
const RETURN_SCOPE_END		= 14;	const bfRETURN_SCOPE_END	= 0x2000;
const RETURN_DELIMETER		= 15;	const bfRETURN_DELIMETER	= 0x4000;
const REGEXP				= 16;	const bfREGEXP				= 0x8000;
const ENDREGEXP				= 17;	const bfENDREGEXP			= 0x10000;

const MAXRULE				= 17;					// Limit

// Precomputed pow(iRule,2) !!
UINT bf[] = {0,1,2,4,8,0x10,0x20,0x40,0x80,0x100,0x200,0x400,0x800,0x1000,0x2000,0x4000,
			0x8000,0x10000};

LPCWSTR szRULES[] = {L"NULL",
					L"LINECOMMENT",
					L"BLOCKCOMMENT",
					L"FUNCTION",
					L"RETURN_EMPTY",
					L"RETURN",
					L"RETURN_FUNCTION",
					L"SCOPE_START",
					L"SCOPE_END",
					L"LITERAL",
					L"ENDLINECOMMENT",
					L"ENDCOMMENT",
					L"RETURN_FN_SCOPE_END",
					L"RETURN_SCOPE_START",
					L"RETURN_SCOPE_END",
					L"RETURN_DELIMETER"};

// Contexts
const CTX_NOCHANGE		= 0;
const CTX_CODE			= 1;
const CTX_LINECOMMENT	= 2;
const CTX_BLOCKCOMMENT	= 3;
const CTX_FUNCTIONBLOCK	= 4;
const CTX_RETURN		= 5;

LPCWSTR szCONTEXTS[] = {	L"CTX_NOCHANGE",
							L"CTX_CODE",
							L"CTX_LINECOMMENT",
							L"CTX_BLOCKCOMMENT",
							L"CTX_FUNCTIONBLOCK",
							L"CTX_RETURN"
						};

typedef std::list<UINT> RULELIST;

struct xContext
{
	RULELIST	m_lstRules;

	virtual DWORD ruleStart(PWCHAR pChar) = 0;
	DWORD getNextRules();
};

static int	f_iScope = 0;
static int	f_iReturnScope = 0;					// For exiting return context
static int	f_iContext = CTX_CODE;
static int  f_nCodeLen = 0;						// Length of script in chars
static int  f_nAdded = 0;						// Number of CHARACTERS added to output
static int	f_nOutputLen = 0;					// Estimated length for output buffer BYTES
static int	f_nRemain = 0;						// Count of unprocess chars

static xContext*	f_pContext = NULL;
static PWCHAR		f_pCode = NULL;
static PWCHAR		f_pOutput = NULL;
static FNSTACK		f_oFnStack; 

typedef std::map<UINT,xContext*> CONTEXTMAP;
CONTEXTMAP	g_mapContexts;

CRITICAL_SECTION g_csInstrument;

/////////////////////////////////////////////////////////////////////////////

int copyToOutput(int nIncCode,WCHAR const * pAdd,int nAddLen)
{
	if (nAddLen)
	{
		wcsncpy(f_pOutput,pAdd,nAddLen);
		f_pOutput += nAddLen;

/*		WCHAR szBuffer[256];
		wcsncpy(szBuffer,pAdd,nAddLen);
		szBuffer[nLen] = 0;
		CString s;
		s.Format(L">>>>>>> Copied : [(%d) chars]=%s\n",nLen,szBuffer);
		ATLTRACE(s);
*/
	}
		
	// incCode != nLen when additional stuff is added
	f_pCode += nIncCode;

	// Make f_pCode look like a proper BSTR
	f_nRemain -= nIncCode;
	long lBSTRChars = f_nRemain * 2;
	memcpy(((PBYTE)f_pCode)-4,&lBSTRChars,4);

	return nAddLen;
}

/////////////////////////////////////////////////////////////////////////////

DWORD xContext::getNextRules()
{
	while (f_nRemain && f_pCode && *f_pCode)
	{
		DWORD dwRules = ruleStart(f_pCode);
		if (dwRules != NORULE) return dwRules;

		if (f_nAdded * 2 >= f_nOutputLen)
			throw JSD_E_PARSE_SCRIPT;

		// No match copy char to output and continue looking
		f_nAdded += ::copyToOutput(1,f_pCode,1);
	}

	return NORULE;
}

typedef struct xCodeContext : public xContext
{
	xCodeContext()
	{
		m_lstRules.push_back(BLOCKCOMMENT);
		m_lstRules.push_back(LINECOMMENT);
		m_lstRules.push_back(LITERAL);
		m_lstRules.push_back(FUNCTION);
		m_lstRules.push_back(RETURN_EMPTY);
		m_lstRules.push_back(RETURN_FUNCTION);
		m_lstRules.push_back(RETURN);
		m_lstRules.push_back(SCOPE_START);
		m_lstRules.push_back(SCOPE_END);	
	}


	virtual DWORD ruleStart(PWCHAR pChar)
	{
		int nChar = *pChar;
		switch(nChar)
		{
			case '/': 
			{
				if (f_nRemain-1 > 0)
				{
					if (*(pChar+1) == '*')
						return bf[BLOCKCOMMENT];

					if (*(pChar+1) == '/')
						return bf[LINECOMMENT];
				}

				break;
			}

			case '\"': return bf[LITERAL];

			case 'f':
			{
				if (f_nRemain-8 > 0 && wcsncmp(L"function",pChar,8) == 0)
					return bf[FUNCTION];

				break;
			}

			case 'r':
			{
				if (f_nRemain-6 > 0 && wcsncmp(L"return",pChar,6) == 0)
					return bf[RETURN_EMPTY] | bf[RETURN] | bf[RETURN_FUNCTION];

				break;
			}

			case '{' : return bf[SCOPE_START];
			case '}' : return bf[SCOPE_END];
		}

		return NORULE;
	}
} CODECONTEXT,*PCODECONTEXT;

struct xLineCommentContext : public xContext
{
	xLineCommentContext()
	{
		m_lstRules.push_back(ENDLINECOMMENT);
	}
	
	virtual DWORD ruleStart(PWCHAR pChar)
	{
		WCHAR nChar = *pChar;
		switch(nChar)
		{
			case '\n': return bf[ENDLINECOMMENT];
		}
			
		return NORULE;
	}
};

struct xRegExpContext : public xContext
{
	xRegExpContext()
	{
		m_lstRules.push_back(ENDREGEXP);
	}
	
	virtual DWORD ruleStart(PWCHAR pChar)
	{
		if (f_nRemain < 1)
			return NORULE;

		WCHAR nChar = *pChar;

		// No need to check that there are any preceeding characters
		// as to get to this context you must have at least 1 char
		WCHAR nPrev = *(pChar-1);

		// If not an escaped / char RegExp ends
		if ('/' == nChar && '\\' != nPrev)  
			return ENDREGEXP;
			
		return NORULE;
	}
};

struct xBlockCommentContext : public xContext
{
	xBlockCommentContext()
	{
		m_lstRules.push_back(ENDCOMMENT);
	}
	
	virtual DWORD ruleStart(PWCHAR pChar)
	{
		WCHAR nChar = *pChar;
		switch(nChar)
		{
			case '*': 
			{
				if (f_nRemain-1)
				{
					if (*(pChar+1) == '/')
						return bf[ENDCOMMENT];
				}

				break;
			}
		}
		
		return NORULE;
	}
};

struct xFunctionReturn : public xContext
{
	xFunctionReturn()
	{
		m_lstRules.push_back(BLOCKCOMMENT);
		m_lstRules.push_back(LINECOMMENT);
		m_lstRules.push_back(LITERAL);
		m_lstRules.push_back(RETURN_SCOPE_START);
		m_lstRules.push_back(RETURN_FN_SCOPE_END);
	}

	virtual DWORD ruleStart(PWCHAR pChar)
	{
		WCHAR nChar = *pChar;
		switch(nChar)
		{
			case '/': 
			{
				if (f_nRemain-1 > 0)
				{
					if (*(pChar+1) == '*')
						return bf[BLOCKCOMMENT];

					if (*(pChar+1) == '/')
						return bf[LINECOMMENT];
				}

				break;
			}

			case '\"': return bf[LITERAL];
			case '{' : return bf[RETURN_SCOPE_START];
			case '}' : return bf[RETURN_FN_SCOPE_END];
		}
		
		return NORULE;
	}
};

struct xReturnContext : public xContext
{
	xReturnContext()
	{
		m_lstRules.push_back(BLOCKCOMMENT);
		m_lstRules.push_back(LINECOMMENT);
		m_lstRules.push_back(LITERAL);
		m_lstRules.push_back(RETURN_SCOPE_START);
		m_lstRules.push_back(RETURN_SCOPE_END);
		m_lstRules.push_back(RETURN_DELIMETER);
	}

	virtual DWORD ruleStart(PWCHAR pChar)
	{
		WCHAR nChar = *pChar;
		switch(nChar)
		{
			case '/': 
			{
				if (f_nRemain-1 > 0)
				{
					if (*(pChar+1) == '*')
						return bf[BLOCKCOMMENT];

					if (*(pChar+1) == '/')
						return bf[LINECOMMENT];
				}

				break;
			}

			case '\"': return bf[LITERAL];
			case '{' : 
			case '(' : return bf[RETURN_SCOPE_START];
			case '}' :
			case ')' : return bf[RETURN_SCOPE_END];
			case ';' : return bf[RETURN_DELIMETER];
		}
		
		return NORULE;
	}
};

////////////////////////////////////////////////////////////////////////////////////////////////
// RULES

class	CRule;							// fwd def
BOOL	g_bInitDone = FALSE;
CRule*	g_aRules[MAXRULE+1];

class CRule 
{
protected:

	IRegExp2Ptr m_pRegExp;
	int			m_iNextContext;

public:
	CRule(LPCWSTR szPattern,int iNextContext)
	{
		// create the regex component
		HRESULT hr = m_pRegExp.CreateInstance(CLSID_RegExp);
		if (SUCCEEDED(hr))
		{
			// Has to be a proper BSTR 
			_bstr_t bstrPattern(szPattern);
			hr = m_pRegExp->put_Pattern(bstrPattern);
		}

		m_iNextContext = iNextContext;

		ATLASSERT(SUCCEEDED(hr));
	}

	virtual void setNextContext(int iContext)
	{
		m_iNextContext = iContext;
	}

	virtual void changeContext()
	{
		// Use whatever context rule indicates
		if (CTX_NOCHANGE != m_iNextContext && m_iNextContext != f_iContext)
		{
			f_iContext = m_iNextContext;
			f_pContext = g_mapContexts[f_iContext];
			ATLTRACE(L"New context is : %s\n",szCONTEXTS[f_iContext]);
		}
	}

	virtual HRESULT getMatch(IMatch2Ptr& pMatch)
	{
		return _getMatch(m_pRegExp,f_pCode,pMatch);
	}

	virtual HRESULT Execute()
	{
		// THIS FAILS IF ITS NOT A BSTR - LPCWSTR IS NO GOOD - THIS WILL MAKE 
		// THE WHOLE THING REALLY INEFFICIENT AS THERE WILL BE A SYSALLOCSTRING 
		// FOR THE WHOLE REMAINDER OF THE FUNCTION EACH TIME.  NEED TO INVESTIGATE
		// THE ATL REG EXP CODE IN ATL7.0 AND IF IT CAN HANDLE LPCWSTR THEN THATS
		// A GOOD ENOUGH REASON TO SWITCH.

		LPCWSTR szLOCATION = L"CRule::Execute";
		HRESULT hRes = E_FAIL;

		try
		{
			IDispatchPtr pDispatch;
			hRes = m_pRegExp->raw_Execute(f_pCode,&pDispatch);
			if (FAILED(hRes)) throw hRes;

			CComQIPtr<IMatchCollection> pCollection = pDispatch;
			long lCount = 0;
			HRESULT hr = pCollection->get_Count(&lCount);
			if (FAILED(hr)) return hr;

			// Rule doesn't match
			if (0 == lCount) return S_FALSE;

			IMatch2Ptr pMatch = pCollection->GetItem(0);
			if (pMatch)
			{
				_bstr_t bstrAdd = (LPCWSTR)pMatch->Value;
				long lLen=0;
				HRESULT hr = pMatch->get_Length(&lLen);
				if (lLen)
				{
					f_nAdded += copyToOutput(lLen,f_pCode,lLen);
					hRes = S_OK;
				}
			}
		}
		catch(HRESULT hr)
		{
			hRes = ReportError(szLOCATION,hr);
		}
		catch(...)
		{
			hRes = ReportError(szLOCATION,E_UNEXPECTED);
		}

		return hRes;
	}
};

class CFunctionDef : public CRule
{
public:
	CFunctionDef(LPCWSTR szPattern,int iNextContext) : CRule(szPattern,iNextContext)
	{
	}

	virtual void LogFunctionDef(LPCWSTR szFn,LPCWSTR szArgs,BOOL bNoTrace,BOOL bForceTrace,BOOL bJSDName)
	{
		CString sAttribs;
		if (bNoTrace)
			sAttribs += L"jsd_no_trace=\"yes\"";
		if (bForceTrace)
			sAttribs += L"jsd_trace=\"yes\"";
		if (bJSDName)
			sAttribs += L"jsd_name=\"yes\"";

		return;
	}

	virtual HRESULT CFunctionDef::Execute()
	{
		HRESULT hRes = E_FAIL;

		try
		{
			IDispatchPtr pDispatch;
			if (SUCCEEDED(m_pRegExp->raw_Execute(f_pCode,&pDispatch)))
			{
				CComQIPtr<IMatchCollection> pCollection = pDispatch;
				long lCount = 0;
				HRESULT hr = pCollection->get_Count(&lCount);
				if (FAILED(hr)) throw E_FAIL;

			 	// The getNextRules method only returns potential matches - where there is more than one
				// rule that could match or a rule that fails to match on the full RegExp we return S_FALSE
				// to continue looking at (all) the other rules.
				if (0 == lCount) return S_FALSE;

				IMatch2Ptr pMatch = pCollection->GetItem(0);
				if (pMatch)
				{
					ISubMatchesPtr pSubs = pMatch->GetSubMatches();
					if (NULL != pSubs && pSubs->GetCount() >= 3)
					{
						_bstr_t bstr$1 = pSubs->GetItem(0);				// fn name
						_bstr_t bstr$2 = pSubs->GetItem(1);				// arguments
		 				_bstr_t bstr$3 = pSubs->GetItem(2);				// between ) and {
 
						// Look for flags to turn on/off fn tracing
						BOOL bForceTrace = FALSE;						// override of global trace flag
						BOOL bNoTrace = FALSE;
						if (bstr$3.length())
						{
							if (wcsstr((LPCWSTR)bstr$3,szNOTRACE))
								bNoTrace = TRUE;

							if (wcsstr((LPCWSTR)bstr$3,szYESTRACE))
								bForceTrace = TRUE;
						}

						CString sFnName((LPCWSTR)bstr$1);
						static iFnCounter = 0;

						CString sUseThisName;
						BOOL bCreatedName = FALSE;
						BOOL bJSDName = FALSE;					// supplied using _JSD_NAME_

						// Use name if it is supplied - but NOT the TRACE flag. This means if you 
						// want to use _JSD_TRACE_ ***AND*** name the function ... you can't !
	 					if (sFnName.GetLength() == 0 && FALSE == bForceTrace)
						{
							sUseThisName = _parseName(bstr$3);

							if (0 == sUseThisName.GetLength())
							{
								bCreatedName = TRUE;
								sUseThisName.Format(L"JScript_Anonymous_Function_%d",iFnCounter++);
							}
							else
								bJSDName = TRUE;
						}

						// NOTE - Use either the name obtained from "arguments" property or take the 
						// name from the comment following the function args, e.g.
						//
						//						var x = function() /* helloMoto */ { ... }
						//
						// Where the "helloMoto" is parsed out by the reg exp and passed as the optional arg, e.g. 
						//
						//						__JSD._TraceFn(arguments,"helloMoto");
						//
						// This only applies to anonymous functions - the real name is not overridden.

						PFNDEF pFn = new FNDEF(sUseThisName,f_iScope,bForceTrace);

						if ((bCreatedName && g_bLogAnonymous) || (FALSE == bCreatedName))
							LogFunctionDef(sUseThisName.GetLength() ? sUseThisName : (LPCWSTR)bstr$1,bstr$2,bNoTrace,bForceTrace,bJSDName);

						// If _JSD_NO_TRACE_ is detected exit now - we could exit where the flag is set but then we
						// wouldn't get the function included in the ParseFunctions output
						if (bNoTrace)
						{
							delete pFn;
							return S_FALSE;
						}

			 			f_oFnStack.push(pFn); 

						/////////////////////////////////////////////////////////////////////////////////////////////////////////////
						// INJECT CODE

						CString sAdd;
						if (sUseThisName.GetLength())
						{
							// Inject the created name as well as this can then be picked up in the stack trace
							if (bCreatedName)
							{
								sAdd.Format(L"function %s%s%s /* %s %s */ {\n\t%s._TraceFn(arguments,\"%s\");",(LPCWSTR)bstr$1,(LPCWSTR)bstr$2,
																(LPCWSTR)bstr$3,(LPCWSTR)sUseThisName,szJSDNAME,szJSDID,(LPCWSTR)sUseThisName);
							}
							else
							{
								sAdd.Format(L"function %s%s%s{\n\t%s._TraceFn(arguments,\"%s\");",(LPCWSTR)bstr$1,(LPCWSTR)bstr$2,
																					(LPCWSTR)bstr$3,szJSDID,(LPCWSTR)sUseThisName);
							}
						}
						else
						{
							// As the parameter to force tracing is optional this supplies an empty string
							LPCWSTR szForce = bForceTrace ? L",true" : L"";
							
							// Have to put in the \n before the "{" otherwise with a def like this:
							//
							//					function foo(x) // comment
							//					{ 
							// We end up with:
							//
							//					function foo(x) // comment{
							//
							//  and the opening brace is then part of the comment !  Can be fixed if necessary.
							
							if (bstr$1.length())
								sAdd.Format(L"function %s%s%s\n{\n\t%s._TraceFn(arguments%s);",(LPCWSTR)bstr$1,(LPCWSTR)bstr$2,
																									(LPCWSTR)bstr$3,szJSDID,szForce);
							else
								sAdd.Format(L"function%s%s\n{\n\t%s._TraceFn(arguments%s);",(LPCWSTR)bstr$2,
																									(LPCWSTR)bstr$3,szJSDID,szForce);
						}

						/////////////////////////////////////////////////////////////////////////////////////////////////////////////

						// Move past consumed part of string
						_bstr_t bstrMatch = (LPCWSTR)pMatch->Value;
						ATLASSERT(bstrMatch.length());

						f_nAdded += copyToOutput(bstrMatch.length(),const_cast<LPWSTR>((LPCWSTR)sAdd),sAdd.GetLength());

						// As we have consumed the "{" need to increment the scope 'manually'
						f_iScope++;	

						hRes = S_OK;
					}
				}
			}
		}
		catch(HRESULT hr)
		{
			hRes = ReportError(L"CFunctionDef::Execute",hr);
		}
		catch(...)
		{
			hRes = ReportError(L"CFunctionDef::Execute",E_UNEXPECTED);
		}

		return hRes;
	}

};

class CReturn : public CRule
{
public:
	CReturn(LPCWSTR szPattern,int iNextContext) : CRule(szPattern,iNextContext)
	{
	}

	virtual HRESULT CReturn::Execute()
	{		
		LPCWSTR szLOCATION = L"CReturn::Execute";

		HRESULT hRes = E_FAIL;

		try
		{
			// When not tracing the function don't add a return statement
			if (f_oFnStack.empty())
				return S_FALSE;

			IMatch2Ptr pMatch;
			HRESULT hr = getMatch(pMatch);
			if (S_OK != hr) return hr;

			// Add the opening part of the return instrumentation
			CString sAdd(L"{var __x=");

			// Move past consumed part of string - just the "return" part so we can add the remainder
			// of "return function(..." 
//			_bstr_t bstrMatch = (LPCWSTR)pMatch->Value;
//			ATLASSERT(bstrMatch.length());

			// This currently allows a redundant ";" after this text has been added.  TODO fix this.
			f_nAdded += copyToOutput(6 /* return */,const_cast<LPWSTR>((LPCWSTR)sAdd),sAdd.GetLength());

			//ATLASSERT(f_iReturnScope == 0);
			if (f_iReturnScope != 0)
				throw JSD_E_PARSE_SCRIPT;

			hRes = S_OK;
		}
		catch(HRESULT hr)
		{
			hRes = ReportError(szLOCATION,hr);
		}
		catch(...)
		{
			hRes = ReportError(szLOCATION,E_UNEXPECTED);
		}

		return hRes;
	}
};

class CReturnEmpty : public CRule
{
public:
	CReturnEmpty(LPCWSTR szPattern,int iNextContext) : CRule(szPattern,iNextContext)
	{
	}

	virtual HRESULT CReturnEmpty::Execute()
	{		
		LPCWSTR szLOCATION = L"CReturnEmpty::Execute";

		HRESULT hRes = E_FAIL;

		try
		{
			// When not tracing the function don't add a return statement
			if (f_oFnStack.empty())
				return S_FALSE;

			IMatch2Ptr pMatch;
			HRESULT hr = getMatch(pMatch);
			if (S_OK != hr) return hr;

			// Add the instrumentation
			PFNDEF pFn = f_oFnStack.top();
			CString sAdd;
			sAdd.Format(L"{%s._TraceFnExit(%s); return;}",szJSDID,(LPCWSTR)pFn->GetExitArgs());

			// Move past consumed part of string
			_bstr_t bstrMatch = (LPCWSTR)pMatch->Value;
			ATLASSERT(bstrMatch.length());

			// This currently allows a redundant ";" after this text has been added.  TODO fix this.
			f_nAdded += copyToOutput(bstrMatch.length(),const_cast<LPWSTR>((LPCWSTR)sAdd),sAdd.GetLength());

//			ATLASSERT(f_iReturnScope == 0);
			if (f_iReturnScope != 0)
				throw JSD_E_PARSE_SCRIPT;

			hRes = S_OK;
		}
		catch(HRESULT hr)
		{
			hRes = ReportError(szLOCATION,hr);
		}
		catch(...)
		{
			hRes = ReportError(szLOCATION,E_UNEXPECTED);
		}

		return hRes;
	}
};

class CScopeStart : public CRule
{
public:
	CScopeStart(LPCWSTR szPattern,int iNextContext) : CRule(szPattern,iNextContext)
	{
	}

	virtual HRESULT Execute()
	{
		HRESULT hr = CRule::Execute();
		if (S_OK == hr)
		{
			f_iScope++;

			ATLTRACE(L"++ Scope = %d\n",f_iScope);
			hr = S_OK;
		}

		return hr;
	}
};

class CScopeEnd : public CRule
{
public:
	CScopeEnd(LPCWSTR szPattern,int iNextContext) : CRule(szPattern,iNextContext)
	{
	}

	virtual HRESULT Execute()
	{
		HRESULT hRes = E_FAIL;

		try
		{
			IMatch2Ptr pMatch;
			HRESULT hr = getMatch(pMatch);
			if (S_OK != hr) return hr;

			f_iScope--;
			ATLTRACE(L"-- Scope = %d\n",f_iScope);
			//ATLASSERT(f_iScope >= 0);
			if (f_iScope < 0)
				throw JSD_E_PARSE_SCRIPT;

			CString sAdd("}");											// The consumed element

			// Is this the end of the current function
			if (false == f_oFnStack.empty())
			{
				PFNDEF pFn = f_oFnStack.top();
				if (pFn->m_iScope == f_iScope)
				{
					f_oFnStack.pop();

					// Put ";" before __JSD to allow for blocks which normally end by "}"
					// e.g. 
					//					function foo() {
					//							dostuff()
					//					}
					// which is ok in JScript but could become
					//					function foo() {
					//						__JSD._TraceFn(...); dostuff() __JSD._TraceFnExit(...);
					//					}
					

					sAdd.Format(L"\t;%s._TraceFnExit(%s);\n}",szJSDID,(LPCWSTR)pFn->GetExitArgs());		// NOTE "}" included here
				}
			}

			f_nAdded += copyToOutput(1,const_cast<LPWSTR>((LPCWSTR)sAdd),sAdd.GetLength());
			hRes = S_OK;
		}
		catch(HRESULT hr)
		{
			hRes = ReportError(L"CScopeEnd::Execute",hr);
		}
		catch(...)
		{
			hRes = ReportError(L"CScopeEnd::Execute",E_UNEXPECTED);
		}

		return hRes;
	}
};


// Because comment can be embedded in one another context need to dynamically set the one
// to return to.
class CBlockComment : public CRule
{
public:
	CBlockComment(LPCWSTR szPattern,int iNextContext) : CRule(szPattern,iNextContext)
	{
	}

	virtual HRESULT CBlockComment::Execute()
	{
		HRESULT hr = CRule::Execute();
		if (S_OK == hr)
		{
			g_aRules[ENDCOMMENT]->setNextContext(f_iContext);
			ATLTRACE(L"ENDCOMMENT will return %s\n",szCONTEXTS[f_iContext]);
		}

		return hr;
	}
};

class CLineComment : public CRule
{
public:
	CLineComment(LPCWSTR szPattern,int iNextContext) : CRule(szPattern,iNextContext)
	{
	}

	virtual HRESULT CLineComment::Execute()
	{
		HRESULT hr = CRule::Execute();
		if (S_OK == hr)
		{
			g_aRules[ENDLINECOMMENT]->setNextContext(f_iContext);
			ATLTRACE(L"ENDLINECOMMENT will return %s\n",szCONTEXTS[f_iContext]);
		}

		return hr;
	}
};

class CReturnScopeStart : public CRule
{
public:
	CReturnScopeStart(LPCWSTR szPattern,int iNextContext) : CRule(szPattern,iNextContext)
	{
	}

	virtual HRESULT CReturnScopeStart::Execute()
	{
		HRESULT hr = CRule::Execute();
		if (S_OK == hr)
		{
			f_iReturnScope++;

			ATLTRACE(L"++ Return Scope = %d\n",f_iReturnScope);
			hr = S_OK;
		}

		return hr;
	}
};

class CReturnScopeEnd : public CRule
{
public:
	CReturnScopeEnd(LPCWSTR szPattern,int iNextContext) : CRule(szPattern,iNextContext)
	{
	}

	virtual HRESULT CReturnScopeEnd::Execute()
	{
		HRESULT hRes = E_FAIL;

		try
		{
			hRes = CRule::Execute();
			if (S_OK == hRes)
			{
				f_iReturnScope--;
				ATLTRACE(L"-- Scope = %d\n",f_iReturnScope);

				if (f_iReturnScope < 0)
					throw JSD_E_PARSE_SCRIPT;	

				//ATLASSERT(f_iReturnScope >= 0);
			}
		}
		catch(HRESULT hr)
		{
			hRes = ReportError(L"CReturnScopeEnd::Execute",hr);
		}
		catch(...)
		{
			hRes = ReportError(L"CReturnScopeEnd::Execute",E_UNEXPECTED);
		}

		return hRes;
	}
};

class CReturnDelimeter : public CRule
{
public:
	CReturnDelimeter(LPCWSTR szPattern,int iNextContext) : CRule(szPattern,iNextContext)
	{
	}

	virtual HRESULT CReturnDelimeter::Execute()
	{
		LPCWSTR szLOCATION = L"CReturnDelimeter::Execute";
		HRESULT hRes = E_FAIL;

		try
		{
			if (f_iReturnScope > 0)
				return S_FALSE;

			//ATLASSERT(f_iReturnScope == 0);
			if (f_iReturnScope != 0)
				throw JSD_E_PARSE_SCRIPT;

			// Add remainder of return instrumentation ";" has already been added by CRule
			PFNDEF pFn = f_oFnStack.top();
			CString sAdd;
			sAdd.Format(L"; %s._TraceFnExit(%s); return __x;}",szJSDID,(LPCWSTR)pFn->GetExitArgs(true));

			f_nAdded += copyToOutput(1,sAdd,sAdd.GetLength());

			hRes = S_OK;
		}
		catch(HRESULT hr)
		{
			hRes = ReportError(szLOCATION,hr);
		}
		catch(...)
		{
			hRes = ReportError(szLOCATION,E_UNEXPECTED);
		}

		return hRes;
	}
};

// When exiting from a "return function" block the "}" is the acceptable delimeter (it doesn't
// need a ";" so decrement the scope then just reuse the CReturnDelimeter functionality.
class CReturnFnScopeEnd : public CReturnDelimeter
{
public:
	CReturnFnScopeEnd(LPCWSTR szPattern,int iNextContext) : CReturnDelimeter(szPattern,iNextContext)
	{
	}

	virtual HRESULT CReturnFnScopeEnd::Execute()
	{
		f_iReturnScope--;
		ATLTRACE(L"-- Scope = %d\n",f_iReturnScope);
		//ATLASSERT(f_iReturnScope >= 0);
		if (f_iReturnScope < 0)
			throw JSD_E_PARSE_SCRIPT;
		
		// Don't forget to add the "}"
		f_nAdded += copyToOutput(1,L"}",1);

		return CReturnDelimeter::Execute();
	}
};

//////////////////////////////////////////////////////////////////////////////////////
// Initialize Contexts and Rules

BOOL init()
{
	// Rules
	g_aRules[NORULE] = NULL;
	g_aRules[LINECOMMENT] = new CLineComment(L"^//",CTX_LINECOMMENT);
	g_aRules[FUNCTION] = new CFunctionDef(L"^\\bfunction(?:\\s)*(\\w*)([^\\)]+\\))([^\\{\\n]*)(\\n)*\\{",CTX_CODE);
	g_aRules[ENDLINECOMMENT] = new CRule(L"^\\n",CTX_CODE);
	g_aRules[ENDCOMMENT] = new CRule(L"^\\*/",CTX_CODE);
	g_aRules[BLOCKCOMMENT] = new CBlockComment(L"^/\\*",CTX_BLOCKCOMMENT);
	g_aRules[SCOPE_START] = new CScopeStart(L"^{",CTX_NOCHANGE);
	g_aRules[SCOPE_END] = new CScopeEnd(L"^}",CTX_NOCHANGE);
	g_aRules[LITERAL] = new CRule(L"^(\"\")|\"(.|\\\\\"|\\\\\\r\\n)*?((\\\\\\\\)+\"|[^\\\\]{1}\")",CTX_NOCHANGE);
	g_aRules[RETURN] = new CReturn(L"^return\\b",CTX_RETURN);
	g_aRules[RETURN_FUNCTION] = new CReturn(L"^return\\s+function",CTX_FUNCTIONBLOCK);
	g_aRules[RETURN_FN_SCOPE_END] = new CReturnFnScopeEnd(L"^}",CTX_CODE);
	g_aRules[RETURN_SCOPE_START] = new CReturnScopeStart(L"^({|\\()",CTX_NOCHANGE);
	g_aRules[RETURN_SCOPE_END] = new CReturnScopeEnd(L"^(}|\\))",CTX_NOCHANGE);
	g_aRules[RETURN_DELIMETER] = new CReturnDelimeter(L"^;",CTX_CODE);
	g_aRules[RETURN_EMPTY] = new CReturnEmpty(L"^return(\\s+;|;)",CTX_NOCHANGE);

	// Contexts
	g_mapContexts[CTX_CODE] = new xCodeContext;
	g_mapContexts[CTX_LINECOMMENT] = new xLineCommentContext;
	g_mapContexts[CTX_BLOCKCOMMENT] = new xBlockCommentContext;
	g_mapContexts[CTX_FUNCTIONBLOCK] = new xFunctionReturn;
	g_mapContexts[CTX_RETURN] = new xReturnContext;
	
	return g_bInitDone = TRUE;
}

BOOL unInit()
{
	// Rules
	for (int i=1; i <= MAXRULE; i++)			// MAXRULE is last index, 0 is NORULE and always NULL
	{
		CRule* pRule = g_aRules[i];
		if (pRule)
		{
			delete pRule;
			g_aRules[i] = NULL;
		}
	}

	// Contexts
	delete g_mapContexts[CTX_CODE];
	delete g_mapContexts[CTX_LINECOMMENT];
	delete g_mapContexts[CTX_BLOCKCOMMENT];
	delete g_mapContexts[CTX_FUNCTIONBLOCK];
	delete g_mapContexts[CTX_RETURN];
	
	g_mapContexts.clear();

	return g_bInitDone = FALSE;
}

//////////////////////////////////////////////////////////////////////////////////////

int countTokens(LPCWSTR szCode,int nLen,LPCWSTR szToken)
{
	int nCount = 0;
	
	PWCHAR p = const_cast<LPWSTR>(szCode);
	PWCHAR pEnd = p + nLen;
	int nTokenLen = wcslen(szToken);
	while(*p && p < pEnd)
	{
		PWCHAR p2 = wcsstr(p,szToken);
		if (NULL == p2) break;

		nCount++;
		p = p2 + nTokenLen + 1;
	}
	
	return nCount;
}

// Count the number of "function" and "return" statements in the code.  This gives the
// required space in the output buffer.  This will overestimate as it doesn't care if
// the tokens are in comments etc.  This is ok.

int estimateSpace(LPCWSTR szCode,int nLen)
{
	// Unfortunately nRETURNLEN has a dynamic element which we can't guess at this point, i.e the length
	// of the original return value. To allow for this just count what is added by the instrumentation and 
	// hope that the additional contingency which is added (based on original code size) is enough. In 
	// practice this will be the case unless very small script files are parsed which have long returns like:
	
	//				return function() { ... /* my long function def */ ... }

	// A simple anonymous function:
	//
	// var x = function() 
	// {	
	//		var x; }
	// }
	//
	// becomes:
	//
	//
	// var x = function ()		
	// /* JScript_Anonymous_Function_XXX */ {									+37
	//		__JSD._TraceFn(arguments,"JScript_Anonymous_Function_XXX");			+60
	//		var x;
	//		;__JSD._TraceFnExit("JScript_Anonymous_Function_XXX",false);		+62
	// }
	
	// Round up a bit for these
	const nRETURNLEN = 65;						
	const nFUNCLEN = 100;

	int nFunctions = countTokens(szCode,nLen,L"function");
	int nReturns = countTokens(szCode,nLen,L"return");
	
	// Each function will also include the implied return statement at the end of its scope
	int nTotal = (nFunctions * nFUNCLEN) + (nFunctions * nRETURNLEN) + (nReturns * nRETURNLEN);

	// Add extra for good measure
	nTotal += (120 * nLen) / 100;

	// Size of CC (and in future any other headers added to code - time written etc
	nTotal += wcslen(szCC);
	
	return nTotal * sizeof(WCHAR);
}

HRESULT ReportParseError(LPCWSTR szLocation,LPCWSTR szCode,int nLen,int nRemain)
{
	try
	{
		int nPos = nLen - nRemain;
		int nStart = nPos-64 < 0 ? 0 : nPos - 64;
		int nEnd = nPos+64 > nLen ? nLen : nPos + 64;

		if (nEnd <= nStart)
			return E_FAIL;

		f_oOutput.OutputString(L"ERROR [PARSER] >>>");
		WCHAR szBuffer[256];
		wsprintf(szBuffer,L"ERROR SCRIPT BEGINS: %s",SafeStrCopy(256-21,szCode));
		CString sBuffer(szBuffer);
		sBuffer.Remove('\n');
		f_oOutput.OutputString(sBuffer);

		// Output some of the code that caused error...
		LPCWSTR szCodeError = szCode + nStart;
		WCHAR szCodeBuffer[128];
		int nCopyLen = nEnd-nStart;
		wcsncpy(szCodeBuffer,szCodeError,nCopyLen);
		szCodeBuffer[nCopyLen] = 0;

		CString sCodeBuffer(szCodeBuffer);
		sCodeBuffer.Remove('\n');

		wsprintf(szBuffer,L"ERROR NEAR: %s",SafeStrCopy(256,sCodeBuffer));
		f_oOutput.OutputString(szBuffer);

		f_oOutput.OutputString(L"ERROR [PARSER] <<<");
	}
	catch(...)
	{
		ATLTRACE(L"Error while reporting error !\n");
	}

	// TODO - report proper error when error message resources are built-in.
	return ReportError(szLocation,E_FAIL);
}


HRESULT _instrumentCode(LPCWSTR szCode,BSTR* pbstrInstrumented)
{
	LPCWSTR szLOCATION = L"_instrumentCode";
	HRESULT hRes = E_FAIL;

	::EnterCriticalSection(&g_csInstrument);

	PWCHAR pszOutput = NULL;

	try
	{
#ifdef _DEBUG
//		::TextToClipboard(szCode);
#endif

		*pbstrInstrumented = 0;
		if (NULL == szCode) return S_FALSE;

		/////////////////////////////////////////////////////////////////////////////////////////////////
		// Allocate output buffers and take a copy of input - as we need to manipulate this buffer
		// to make it look like a proper BSTR.

		f_nCodeLen = wcslen(szCode);
		if (0 == f_nCodeLen) return S_FALSE;

		f_nAdded = 0;
		f_nRemain = f_nCodeLen;

		f_nOutputLen = estimateSpace(szCode,f_nCodeLen);
		pszOutput = (PWCHAR)::HeapAlloc(GetProcessHeap(),0,f_nOutputLen);
		if (f_nOutputLen != HeapSize(GetProcessHeap(),0,pszOutput))
			return E_FAIL;

		int nFakeLen = 2 * (f_nCodeLen + 1) + 4;		// 4 bytes for fake BSTR length
		f_pCode = (PWCHAR)(PWCHAR)::HeapAlloc(GetProcessHeap(),0,nFakeLen);
		if (nFakeLen != HeapSize(GetProcessHeap(),0,f_pCode))
			return E_FAIL;

		memcpy(((PBYTE)f_pCode)+4,szCode,2 * (f_nCodeLen + 1));

		long lBSTRChars = f_nCodeLen * 2;
		memcpy(((PBYTE)f_pCode),&lBSTRChars,4);		
		
		/////////////////////////////////////////////////////////////////////////////////////////////////
		// Initialize and start parsing

		if ( FALSE == g_bInitDone)
			g_bInitDone = init();

		f_iScope = 0;
		f_iReturnScope = 0;					// For exiting return context
		f_iContext = CTX_CODE;

		// Empty the fn stack
		while (f_oFnStack.size())
		{
			PFNDEF p = f_oFnStack.top();
			f_oFnStack.pop();
			delete p;
		}

		f_pCode += 2;
		f_pOutput = pszOutput;
		f_pContext = g_mapContexts[f_iContext];
		ATLTRACE(L"Context is : %s\n",szCONTEXTS[f_iContext]);

		/////////////////////////////////////////////////////////////////////////////////////////////////////
		// Write code headers
		
		// Add the conditional compilation flag
		f_nAdded = copyToOutput(0,szCC,wcslen(szCC));

		/////////////////////////////////////////////////////////////////////////////////////////////////////
		
		DWORD dwRuleMatches = 0; 
		hRes = S_OK;					
		while (dwRuleMatches = f_pContext->getNextRules())
		{
			// At this point we should have a or more rules.  One of which should match
			RULELIST::iterator iter = f_pContext->m_lstRules.begin();
			
			while (iter != f_pContext->m_lstRules.end())
			{
				int iRule = *iter;
				
				// Look for the matching rules
				if ((dwRuleMatches & bf[iRule]) == 0)
				{
					iter++;
					continue;
				}

				ATLTRACE(L"Trying %s...\n",szRULES[iRule]);
				hRes = g_aRules[iRule]->Execute();
				if (FAILED(hRes)) 
					throw hRes;

				if (S_OK == hRes)
				{		
					ATLTRACE(L"...Matches\n");
					g_aRules[iRule]->changeContext();
					break;
				}

				ATLTRACE(L"...No Match\n");
				iter++;
			}

			// No rule matched so bump up to next char then try again.
			if (S_FALSE == hRes)
				f_nAdded += ::copyToOutput(1,f_pCode,1);
		}
		
		//ATLASSERT(f_nRemain == 0);
		if (f_nRemain != 0 || f_iReturnScope > 0 ||  f_iScope > 0)
			throw JSD_E_PARSE_SCRIPT;

		pszOutput[f_nAdded] = '\0';
		*pbstrInstrumented = ::SysAllocString(pszOutput);

#ifdef _DEBUG
//		::TextToClipboard(*pbstrInstrumented);
#endif
		// hRes should have been set to S_OK above if parsing was successful - but maybe S_FALSE;
		if (SUCCEEDED(hRes))
			hRes = S_OK;
	}
	catch(HRESULT hr)
	{
		if (JSD_E_PARSE_SCRIPT == hRes)
			hRes = ReportParseError(szLOCATION,szCode,f_nCodeLen,f_nRemain);
		else
			hRes = ReportError(szLOCATION,hr);
	}
	catch(...)
	{
		hRes = ReportError(szLOCATION,E_UNEXPECTED);
	}

	if (pszOutput)
	try {
		ATLASSERT(f_nAdded * 2 < f_nOutputLen);
		HeapFree(GetProcessHeap(),0,pszOutput);
	} catch(...) {}

	unInit();

	::LeaveCriticalSection(&g_csInstrument);

	return hRes;
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
Switzerland Switzerland
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions