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

Commit

Permalink
Merge pull request #8 from nbcnews/xml_parse
Browse files Browse the repository at this point in the history
- Validate component's xml files
    - Warn about absent required attributes
    - Warn about attributes referencing undefined functions
-Fix syntax parsing issues
    - Float constants recognized with uppercase exponent: 1.2E10, 2D-2
    - Hexadecimal constants can contain lowercase letters: &habcd
    - Allow function as return type
    - Resolve ambiguity in print statement followed by spaces and :
    - Attribute name following @ operator can be reserved word: xml@for, xml@true
- Additional validations
    - Detect calls to undefined functions
    - Detect name collisions between functions and local variables
  • Loading branch information
j-denisb authored Nov 25, 2019
2 parents 1c17e18 + 0ef4f87 commit 0a88835
Show file tree
Hide file tree
Showing 10 changed files with 649 additions and 100 deletions.
2 changes: 1 addition & 1 deletion ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ function print_items(t) {
return items.concat(print_separators(t[3]))
}
function print_separators(t) {
return t.filter(f=>f).map(e => {
return (t||[]).filter(f=>f).map(e => {
return {
node: 'separator',
val: e.text,
Expand Down
15 changes: 10 additions & 5 deletions brs.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let lexer = moo.compile({
reserved: ['end','if','else','elseif','exit','not','and','or','return','function','sub','print']
})
},
number: /\d+%|\d*\.?\d+(?:[ed][+-]?\d+)?[!#&]?|&h[0-9ABCDEF]+/,
number: /\d+%|\d*\.?\d+(?:[edED][+-]?\d+)?[!#&]?|&h[0-9ABCDEFabcdef]+/,
string: /"(?:[^"\n\r]*(?:"")*)*"/,
op: /<>|<=|>=|<<|>>|\+=|-=|\*=|\/=|\\=|<<=|>>=/,
othr: /./
Expand Down Expand Up @@ -97,7 +97,6 @@ var grammar = {
{"name": "param_default", "symbols": ["_", {"literal":"="}, "_", "rval"]},
{"name": "param_type", "symbols": ["_", {"literal":"as"}, "__", "ptype"]},
{"name": "ptype", "symbols": ["type"], "postprocess": u},
{"name": "ptype", "symbols": [{"literal":"function"}], "postprocess": id},
{"name": "rtype", "symbols": ["type"], "postprocess": u},
{"name": "rtype", "symbols": [{"literal":"void"}], "postprocess": id},
{"name": "type", "symbols": [{"literal":"boolean"}]},
Expand All @@ -108,6 +107,7 @@ var grammar = {
{"name": "type", "symbols": [{"literal":"string"}]},
{"name": "type", "symbols": [{"literal":"object"}]},
{"name": "type", "symbols": [{"literal":"dynamic"}]},
{"name": "type", "symbols": [{"literal":"function"}]},
{"name": "statement_list$ebnf$1", "symbols": []},
{"name": "statement_list$ebnf$1$subexpression$1", "symbols": ["statement_separators", "statement"]},
{"name": "statement_list$ebnf$1", "symbols": ["statement_list$ebnf$1", "statement_list$ebnf$1$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
Expand Down Expand Up @@ -209,8 +209,11 @@ var grammar = {
{"name": "print_items$ebnf$3$subexpression$1", "symbols": ["_", "PEXPR"]},
{"name": "print_items$ebnf$3$subexpression$1", "symbols": ["pxsp", "EXPR"]},
{"name": "print_items$ebnf$3", "symbols": ["print_items$ebnf$3", "print_items$ebnf$3$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
{"name": "print_items$ebnf$4", "symbols": []},
{"name": "print_items$ebnf$4", "symbols": ["print_items$ebnf$4", "psep"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
{"name": "print_items$ebnf$4$subexpression$1$ebnf$1", "symbols": []},
{"name": "print_items$ebnf$4$subexpression$1$ebnf$1", "symbols": ["print_items$ebnf$4$subexpression$1$ebnf$1", "psep"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
{"name": "print_items$ebnf$4$subexpression$1", "symbols": ["print_items$ebnf$4$subexpression$1$ebnf$1", "ppp"]},
{"name": "print_items$ebnf$4", "symbols": ["print_items$ebnf$4$subexpression$1"], "postprocess": id},
{"name": "print_items$ebnf$4", "symbols": [], "postprocess": function(d) {return null;}},
{"name": "print_items", "symbols": ["print_items$ebnf$2", "EXPR", "print_items$ebnf$3", "print_items$ebnf$4"], "postprocess": ast.print_items},
{"name": "psep", "symbols": [{"literal":";"}], "postprocess": id},
{"name": "psep", "symbols": [{"literal":","}], "postprocess": id},
Expand Down Expand Up @@ -249,11 +252,13 @@ var grammar = {
{"name": "call$ebnf$1", "symbols": ["call$ebnf$1", "call$ebnf$1$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
{"name": "call", "symbols": ["_", {"literal":"("}, "_", "rval", "call$ebnf$1", "_", {"literal":")"}], "postprocess": ast.call},
{"name": "call", "symbols": ["_", {"literal":"("}, "_", {"literal":")"}], "postprocess": ast.call},
{"name": "xmlattr", "symbols": ["_", {"literal":"@"}, "_", "IDENTIFIER"], "postprocess": ast.xmlattr},
{"name": "xmlattr", "symbols": ["_", {"literal":"@"}, "_", "ATTR_NAME"], "postprocess": ast.xmlattr},
{"name": "PROP_NAME", "symbols": ["IDENTIFIER"], "postprocess": ast.name},
{"name": "PROP_NAME", "symbols": ["RESERVED"], "postprocess": ast.name},
{"name": "PROP_NAME", "symbols": ["constant"], "postprocess": ast.name},
{"name": "PROP_NAME", "symbols": ["string"], "postprocess": ast.name},
{"name": "ATTR_NAME", "symbols": ["IDENTIFIER"], "postprocess": ast.name},
{"name": "ATTR_NAME", "symbols": ["RESERVED"], "postprocess": ast.name},
{"name": "access_or_call", "symbols": ["access"], "postprocess": id},
{"name": "access_or_call", "symbols": ["call"], "postprocess": id},
{"name": "picx", "symbols": ["access"], "postprocess": id},
Expand Down
13 changes: 8 additions & 5 deletions brs.ne
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ let lexer = moo.compile({
reserved: ['end','if','else','elseif','exit','not','and','or','return','function','sub','print']
})
},
number: /\d+%|\d*\.?\d+(?:[ed][+-]?\d+)?[!#&]?|&h[0-9ABCDEF]+/,
number: /\d+%|\d*\.?\d+(?:[edED][+-]?\d+)?[!#&]?|&h[0-9ABCDEFabcdef]+/,
string: /"(?:[^"\n\r]*(?:"")*)*"/,
op: /<>|<=|>=|<<|>>|\+=|-=|\*=|\/=|\\=|<<=|>>=/,
othr: /./
Expand Down Expand Up @@ -79,9 +79,9 @@ param_default -> _ "=" _ rval

param_type -> _ "as" __ ptype

ptype -> type {% u %} | "function" {% id %}
ptype -> type {% u %}
rtype -> type {% u %} | "void" {% id %}
type -> "boolean" | "integer" | "longinteger" | "float" | "double" | "string" | "object" | "dynamic"
type -> "boolean" | "integer" | "longinteger" | "float" | "double" | "string" | "object" | "dynamic" | "function"
# "Interface" is not allowed in param or return

statement_list ->
Expand Down Expand Up @@ -187,7 +187,7 @@ print -> "print"
| "?" {% id %}
print_items ->
psep:* {% id %}
| psep:* EXPR (_ PEXPR | pxsp EXPR):* psep:* {% ast.print_items %}
| psep:* EXPR (_ PEXPR | pxsp EXPR):* (psep:* ppp):? {% ast.print_items %}
psep-> ";" {%id%} | "," {%id%} | __ {%id%}
ppp-> ";" {%id%} | "," {%id%}
pxsp-> _ ppp psep:* {% flat %}
Expand All @@ -208,13 +208,16 @@ access ->
| _ "[" _ EXPR (_ "," _ EXPR):* _ "]" {% ast.index %}
call -> _ "(" _ rval (_ "," _ rval):* _ ")" {% ast.call %}
| _ "(" _ ")" {% ast.call %}
xmlattr -> _ "@" _ IDENTIFIER {% ast.xmlattr %}
xmlattr -> _ "@" _ ATTR_NAME {% ast.xmlattr %}

PROP_NAME ->
IDENTIFIER {% ast.name %}
| RESERVED {% ast.name %}
| constant {% ast.name %}
| string {% ast.name %}
ATTR_NAME ->
IDENTIFIER {% ast.name %}
| RESERVED {% ast.name %}
access_or_call ->
access {% id %}
| call {% id %}
Expand Down
67 changes: 42 additions & 25 deletions brslint.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const nearley = require("nearley")
const grammar = require("./brs")
const preprocessor = require("./preprocessor")

let errors = [], warnings = [], globals = []
let warnings = [], globals = [], scoped = {}

function traverse(node, callback, ctx) {
if (!node) return
Expand Down Expand Up @@ -54,46 +54,58 @@ function traverseRule(node, rule, warnings) {
}

function unassignedVar(node, vars) {
const defineVar = (name, node) => {
const lookupName = name.toLowerCase()
// User defined functions collide with local variables, global functions do not
if (scoped.has(lookupName)) {
const token = node.token || node.tokens[0]
warnings.push({ message: 'Name `' + name + '` is used by function in ' + scoped.get(lookupName).file, loc: token.line+','+token.col, level: 1 })
}
vars.push(lookupName)
}

if (node.node === 'function' || node.node === 'sub') {
return [ 'm', 'true', 'false' ]
}
if (node.node == 'param') {
vars.push(node.name.toLowerCase())
defineVar(node.name, node)
}
if (node.node == '=' && node.lval) {
if (!node.lval.accessors) {
vars.push(node.lval.val.toLowerCase())
defineVar(node.lval.val, node.lval)
}
}
if (node.node == 'dim') {
vars.push(node.name.val.toLowerCase())
defineVar(node.name.val, node.name)
}
if (node.node == 'foreach') {
vars.push(node.var.val.toLowerCase())
defineVar(node.var.val, node.var)
}
if (node.node == 'for') {
vars.push(node.var.val.toLowerCase())
defineVar(node.var.val, node.var)
}
if (node.node == 'id' && (node.accessors == null || node.accessors[0].node != 'call')) {
if (vars.indexOf(node.val.toLowerCase()) < 0 && globals.indexOf(node.val) < 0) {
warnings.push({ message: 'Undefined variable \'' + node.val + '\'', loc: node.li.line+','+node.li.col, level: 1 })
return
if (node.node == 'id') {
const lookupName = node.val.toLowerCase()
if (vars.indexOf(lookupName) < 0 && !globals.has(lookupName) && !scoped.has(lookupName)) {
if (node.accessors && node.accessors[0].node == 'call') {
warnings.push({ message: 'Undefined function \'' + node.val + '\'', loc: node.li.line+','+node.li.col, level: 1 })
} else {
warnings.push({ message: 'Undefined variable \'' + node.val + '\'', loc: node.li.line+','+node.li.col, level: 1 })
}
}
}
}

let parser
module.exports = {
parse: function (input, options) {
options = options || {}
errors = []
let errors = []
try {
if (options.preprocessor) {
input = preprocessor(input, options.consts)
}

parser = new nearley.Parser(nearley.Grammar.fromCompiled(grammar),{ keepHistory: options.debug })
let parser = new nearley.Parser(nearley.Grammar.fromCompiled(grammar),{ keepHistory: options.debug })
parser.feed(input)

if (parser.results.length > 1) {
Expand All @@ -103,27 +115,32 @@ module.exports = {
if (options.ast != null) {
console.log(JSON.stringify(parser.results[0]))
}

return { ast: parser.results[0], errors: errors }
}
catch (x) {
errors.push(x.message)
return { success: false, errors: errors }
const regex = / at line (\d+) col (\d+):\n\n\s*/i
let loc = x.token.line + ',' + x.token.col
let message = x.message.replace(regex, ': `')
message = message.replace(/\n\s*\^\n/, '`. ').replace(/\n/, '')
errors.push({ level: 0, message: message, loc: loc})
return { errors: errors }
}

return { success: true, ast: parser.results[0], errors: errors }
},
style: function (f, g) {

check: function (ast, scopedFunctions, globalFunctions) {
warnings = []
globals = g
traverse(f, unassignedVar)
globals = globalFunctions
scoped = scopedFunctions
traverse(ast, unassignedVar)
return warnings
},

lint: (ast, g, rules) => {
let w = []
lint: (ast, rules) => {
let warnings = []
for (let rule of rules) {
traverseRule(ast, rule, w)
traverseRule(ast, rule, warnings)
}
return w
return warnings
}
}

Loading

0 comments on commit 0a88835

Please sign in to comment.