Skip to content
This repository has been archived by the owner on Sep 25, 2018. It is now read-only.

Scjson support #302

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6302696
Decoupled scxml analyzer
feyzo Nov 21, 2014
a1920a9
Expression check ready + half of condition check
feyzo Nov 21, 2014
d085006
Check assignment without '=' sign
feyzo Nov 21, 2014
c962320
Changed loops
feyzo Nov 21, 2014
f2b745b
Included underscore
feyzo Nov 21, 2014
3719e6f
Working transitions and test 309 not erroring
feyzo Nov 21, 2014
bbf6870
Changed from recursive to iterative
feyzo Nov 25, 2014
49aec5a
Transition "cond" bugfix
feyzo Nov 25, 2014
627e2b7
File inlining complete
feyzo Nov 26, 2014
4236ba0
Validate "script" contents
feyzo Nov 26, 2014
898f0b2
Small cleanup
feyzo Nov 26, 2014
0ef2b81
Readonly system variables
feyzo Nov 26, 2014
a7e79e8
Fix root script tag not raising error
feyzo Nov 26, 2014
3b395cc
Added new tests
feyzo Nov 26, 2014
e06e22a
Fixed a typo
feyzo Nov 26, 2014
79f17b4
Removed underscore and cloning
feyzo Nov 27, 2014
a72feb8
Removed scxml analyzer
feyzo Nov 27, 2014
be7a123
Forced override value is now string. Added test309
feyzo Nov 27, 2014
f927b07
Traversing javascript syntax tree
feyzo Nov 27, 2014
86ebf19
Fixed an error on illegal datamodel expression
feyzo Nov 27, 2014
c7e4e1e
Log expressions are validated
feyzo Nov 27, 2014
68b7a7e
Fixed test153
feyzo Nov 27, 2014
cf9dea1
Fixed sendType and test199
feyzo Nov 27, 2014
3b84b7c
Added test199 to list
feyzo Nov 27, 2014
4d381eb
Foreach validation
feyzo Nov 29, 2014
3f4a507
Changed structure
feyzo Nov 29, 2014
cd658b6
More foreach support
feyzo Nov 29, 2014
d5a110b
Bugfixes and transition improvements
feyzo Nov 29, 2014
0dad3af
Couple fixes
feyzo Nov 29, 2014
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/compiler/scxml-to-scjson.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ function transform(xmlString){

//console.log('action node',node);

if(node.name === "send") {
//Revert overriding attribute name "type".
//Made this workaround because we use parameter "type" within the code everywhere.
//If "send" tag has attribute "type" it gets merged above and breaks tag name.
if(node.attributes.type && node.attributes.type.value) {
action.sendtype = node.attributes.type.value;
action.type = "send";
}
}

var actionContainer;
if(Array.isArray(currentJson)){
//this will be onExit and onEntry
Expand Down
341 changes: 341 additions & 0 deletions lib/compiler/static-analysis/scjson-analyzer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
'use strict';

var esprima = require('esprima');
var fileUtils = require('./file-utils');

var systemVariables = ["_event", "_sessionid", "_name", "_ioprocessors", "_x"];

var scJsonAnalyzer = {
analyze: function(scJson, docUrl, context, done) {
var changes = [],
asyncCount = 0,
waitingForAsync = false;

function processState(state) {
if(state.datamodel) processActions(state, 'datamodel');
if(state.onExit) processActions(state, 'onExit');
if(state.onEntry) processActions(state, 'onEntry');
if(state.transitions) {
processActions(state, 'transitions', state);

state.transitions.forEach(function(transition, i){
if(transition.onTransition) {
processActions(transition, 'onTransition');
}
});
}

if(state.rootScripts) {
processActions(state, 'rootScripts');
}

if(state.states) state.states.forEach(function(substate, i){ processState(substate); });
}

function processActions(actionContainer, name, state){
if(Array.isArray(actionContainer[name])) {

Object.keys(actionContainer[name]).forEach(function(i) {
checkAction(actionContainer[name], i, actionContainer[name][i].type || name, state);

if(actionContainer[name][i].actions) processActions(actionContainer[name][i], 'actions');
});
} else {
checkAction(actionContainer, name, name, state);
}
}

function checkAction(action, propertyName, type, state) {
if(actionTags[type]) {

var errors = actionTags[type](action[propertyName], function (errors, override) {
if(override) {
handleError(action, propertyName, errors, type, state, override);
} else if(errors.length > 0) {
handleError(action, propertyName, errors, type, state);
}

asyncDone();
});

if(errors) {
if(errors.override) {
handleError(action, propertyName, errors.errors, type, state, errors.override);
} else if(errors.length > 0) {
handleError(action, propertyName, errors, type, state);
}
}
}
}

var actionTags = {
'data': function (node) {
return validateJavascriptAssignment(node.id, node.expr);
},
'assign': function (node) {
if(node.location && node.expr) {
return validateJavascriptAssignment(node.location, node.expr);
}

return [];
},
'transitions': function (node) {
if(node.cond) {
// return validateJavascriptCondition(node.cond);
var errors = validateJavascriptCondition(node.cond);

if(errors.length) {
//Assume illegal booleans as false, send override
//https://github.com/jbeard4/scxml-test-framework/blob/2.0.0/test/w3c-ecma/test309.txml.scxml
node.cond.expr = 'false';

return { override: node, errors: errors };
}
}

return [];
},
'if': function (node) {
return validateJavascriptCondition(node.cond);
},
'ifelse': function (node) {
return validateJavascriptCondition(node.cond);
},
'script': function (node, done) {
if(node.src) {
asyncStarted();

getFileContents(node.src, function (error, content) {
if(error) {
done([error]);
} else {
var errors = validateArbitraryJavascript(content);

if(errors.length > 0) {
done(errors);
} else {
delete node.src;
node.content = content;
done(null, node);
}
}
});
}

if(node.content) {
return validateArbitraryJavascript(node.content);
}
},
'log': function (node) {
if(node.expr) {
return validateJavascriptExpression(node.expr);
}

return [];
},
'send': function (node) {
if(node.type) {
return validateJavascriptExpression(node.expr);
}

return [];
},
'foreach': function (node) {
var errors = [];

if(node.item) {
var results = validateJavascriptIdentifier(node.item);

if(results && results.length > 0)
{
errors = errors.concat(results);
}
}

if(node.index) {
var results = validateJavascriptIdentifier(node.index);

if(results && results.length > 0)
{
errors = errors.concat(results);
}
}

if(node.array) {
var results = validateJavascriptExpression(node.array);

if(results && results.length > 0)
{
errors = errors.concat(results);
}
}

return errors;
}
};

function validateJavascriptAssignment(leftHand, rightHand) {
var errors = [];

var leftHandCheck = validateArbitraryJavascript(leftHand);
var rightHandCheck = validateArbitraryJavascript(rightHand);

if(leftHandCheck.length) {
errors.push(leftHandCheck);
} else if(rightHandCheck.length) {
errors.push(rightHandCheck);
} else if(systemVariables.indexOf(extractJavascript(leftHand)) !== -1) {
errors.push('You can\'t change system variables: ' + leftHand);
}

return errors;
}

function validateJavascriptCondition(condition) {
return validateArbitraryJavascript(condition);
}

function validateJavascriptExpression (js) {
return validateArbitraryJavascript(js);
}

function validateJavascriptIdentifier (js) {
js = extractJavascript(js);

var errors = validateArbitraryJavascript(js);

if(errors.length) return errors;

var syntaxTree = esprima.parse(js, {});

if(syntaxTree.body[0].expression.type !== 'Identifier') {
return ['Illegal identifier: ' + js];
}
}

function validateArbitraryJavascript (js) {
js = extractJavascript(js);

var errors = [];

try {
var syntaxTree = esprima.parse(js, {});

traverseSyntaxTree(syntaxTree, errors);
} catch (e) {
errors.push(e.description);
}

return errors;
}

var treeTypes = {
"AssignmentExpression": function(tree, errors) {
//Check if assignee is a system variable
if(systemVariables.indexOf(tree.expression.left.name) !== -1) {
errors.push('You can\'t change system variables: ' + tree.expression.left.name);
}
}
};

function traverseSyntaxTree(tree, errors) {
Object.keys(tree).forEach(function(i){
if (tree[i] && typeof(tree[i]) === 'object') {
if (tree[i].type && treeTypes[tree[i].type]) {
treeTypes[tree[i].type](tree, errors);
}

//Go deeper into the child nodes
traverseSyntaxTree(tree[i], errors);
}
});
}

function getFileContents(filePath, done) {
//docUrl and context are coming from top function.
fileUtils.read(filePath, docUrl, context, function (fileContent) {
done(fileContent.error, fileContent.content);
});
}

function handleError (node, property, errors, type, state, override) {
var errorNode = {
$line : node[property].$line,
$column : node[property].$column,
type: 'raise',
event: 'error.execution',
data: {
message: errors ? errors.join(', ') : ''
}
};

changes.push({
old: node,
prop: property,
type: type,
new: override,
state: state,
error: errorNode
});
}

function extractJavascript (attribute) {
//Just a workaround for esprima parsing.
if (typeof(attribute) === 'object') {
attribute = attribute.expr;
}

return attribute;
}

function commitChanges (scJson) {
changes.forEach(function (change) {

if(change.type === 'data') {
delete scJson.datamodel;
scJson.onEntry = [ change.new || change.error ];
} else if(change.type === 'script' && !change.new && scJson.rootScripts) {
delete scJson.rootScripts;
scJson.onEntry = [ change.error ];
} else if(change.type === 'transitions') {
if(!change.state.onEntry) change.state.onEntry = [];

change.state.onEntry.push(change.error);

change.old[change.prop] = change.new;
} else {
change.old[change.prop] = change.new || change.error;
}
});
}

function asyncStarted () {
asyncCount++;
}

function asyncDone () {
asyncCount--;

//If we are only waiting for async processes
if(waitingForAsync && asyncCount === 0) {
completeAnalysis();
}
}

function completeAnalysis () {
commitChanges(scJson);
done({ scJson: scJson, errors: [] });
}

processState(scJson, 'scJson');

if(asyncCount === 0) {
completeAnalysis();
} else {
//Wait for async processes to end
waitingForAsync = true;
}
}
};

module.exports = scJsonAnalyzer;
Loading