/***************************************************************************
*
* 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;
}