├── .gitignore ├── .babelrc ├── package.json ├── test └── safely.js ├── README.md └── lib └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ], 4 | "plugins": ["transform-safely"] 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": {}, 3 | "description": "Transform code for safe property access and modification", 4 | "devDependencies": { 5 | "babel-cli": "^6.11.4", 6 | "babel-helper-plugin-test-runner": "^6.8.0" 7 | }, 8 | "directories": {}, 9 | "keywords": [ 10 | "babel-plugin" 11 | ], 12 | "license": "MIT", 13 | "main": "lib/index.js", 14 | "maintainers": [ 15 | { 16 | "email": "kriszyp@gmail.com", 17 | "name": "kriszyp" 18 | } 19 | ], 20 | "name": "babel-plugin-transform-safely", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/kriszyp/babel-plugin-transform-alkali/tree/master/babel-plugin-transform-safely" 24 | }, 25 | "scripts": {}, 26 | "version": "0.1.2" 27 | } 28 | -------------------------------------------------------------------------------- /test/safely.js: -------------------------------------------------------------------------------- 1 | var obj = {foo: 'bar'} 2 | var empty = null 3 | var func = null 4 | var maybeArray = null 5 | var tests = { 6 | basic: function() { 7 | console.assert(safely(empty.b.c) === undefined) 8 | }, 9 | assign: function() { 10 | safely(empty.b.c.d = 'hi') 11 | }, 12 | call: function() { 13 | safely(empty.b.someFunction('hi').c) 14 | }, 15 | computedMember: function() { 16 | safely(empty[obj.foo].b) 17 | }, 18 | simpleCall: function() { 19 | safely(func(3)) 20 | }, 21 | funcExpr: function() { 22 | safely((obj && func)(3)) 23 | }, 24 | numericAssignment: function() { 25 | safely(maybeArray[0] = 'hi') 26 | }, 27 | push: function() { 28 | safely(maybeArray.subArray.push('hello')) 29 | }, 30 | combo1: function() { 31 | safely(empty.b = obj.foo.bar(func.c)) 32 | }, 33 | arrow: function() { 34 | return { 35 | get: (row) => safely(row.SelectedOutcome.FieldType) 36 | } 37 | }, 38 | thisAssignment: function() { 39 | safely(this.obj['a'] ='b') 40 | } 41 | } 42 | var test 43 | for (var testName in tests) { 44 | var result = require("babel-core").transform('test=' + tests[testName].toString(), { 45 | plugins: ["transform-safely"] 46 | }) 47 | console.log('transformed', result.code) 48 | eval(result.code)() 49 | } 50 | 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-transform-safely 2 | This babel plugin will transform expressions that use a `safely` keyword/call to produce safe property access and modification. 3 | 4 | ## Installation 5 | 6 | ```sh 7 | $ npm install babel-plugin-transform-safely 8 | ``` 9 | 10 | ## Usage 11 | 12 | The basic format of using the transform is to write object-checked, safe expressions (existential property access) in the form: 13 | ``` 14 | safely(expression) 15 | ``` 16 | We can then access properties on variables that may be set to null or undefined, and they won't error out. For example: 17 | ``` 18 | safely(object.subObject.subProperty) // will check for each object's existence before accessing property 19 | ``` 20 | Will be rewritten to: 21 | ``` 22 | var _object 23 | (_object = object == null ? void 0 : object.subObject) == null ? void 0 : _object.subProperty 24 | ``` 25 | So if `object` or `subObject` is not an object, the entire expression will return `undefined` (without an error). 26 | 27 | And we can assign properties to objects thay may not exist yet, and they will be created: 28 | ``` 29 | safely(object.subObject.subProperty = 4) // will create the any missing objects in order to assign property 30 | ``` 31 | If `object` or `subObject` are not objects, they will be assigned an object. 32 | 33 | And we can make function or method calls on functions may or may not exist as well: 34 | ``` 35 | safely(object.method(args)) // will only call if method exists 36 | ``` 37 | Again, this will return `undefined` if the method doesn't exist. 38 | 39 | If you use a numeric index or the `push` or `unshift` method, the code will be transformed to create an array as necessary (rather than just doing an existence check). For example: 40 | ``` 41 | object.arrayProperty.push('hi') 42 | ``` 43 | If `arrayProperty` is not defined, an array will be created (and `push` called on it). 44 | 45 | And of course you can combine any permutation of the above (with any other valid expression or operator): 46 | ``` 47 | safely(empty.b.c = object[a]() || object[b](something.c.d)) 48 | ``` 49 | 50 | ## Transform Usage 51 | 52 | ### Via `.babelrc` (Recommended) 53 | 54 | **.babelrc** 55 | 56 | ```json 57 | { 58 | "plugins": ["transform-safely"] 59 | } 60 | ``` 61 | 62 | ### Via CLI 63 | 64 | ```sh 65 | $ babel --plugins transform-safely 66 | ``` 67 | 68 | ### Via Node API 69 | 70 | ```javascript 71 | require("babel-core").transform("code", { 72 | plugins: ["transform-safely"] 73 | }); 74 | ``` 75 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function ({ types: t }) { 2 | 3 | function getObjectReference(path, name) { 4 | if (!path.objectRefId) { 5 | path.objectRefId = path.scope.generateDeclaredUidIdentifier(name || 'object') 6 | } 7 | return path.objectRefId 8 | } 9 | 10 | function getTempId(scope) { 11 | let id = scope.path.getData("functionBind"); 12 | if (id) return id; 13 | 14 | id = scope.generateDeclaredUidIdentifier("context"); 15 | return scope.path.setData("functionBind", id); 16 | } 17 | 18 | function markAsSafe(node) { 19 | node.isSafe = node 20 | return node 21 | } 22 | 23 | function ensureObject(path, isArray) { 24 | let { node } = path 25 | if (t.isIdentifier(node)) { 26 | // (object || (object = {})).property = right 27 | path.replaceWith(t.logicalExpression('||', node, t.assignmentExpression('=', node, isArray ? t.arrayExpression([]) : t.objectExpression([])))) 28 | } else if (t.isMemberExpression(node)) { 29 | let ensuredObject = ensureObject(path.get('object'), t.isNumericLiteral(node.property)) 30 | let objectRef = getObjectReference(path.get('object')) 31 | path.replaceWith(t.logicalExpression('||', 32 | markAsSafe(t.memberExpression(t.assignmentExpression('=', objectRef, ensuredObject), node.property, node.computed)), 33 | markAsSafe(t.assignmentExpression('=', markAsSafe(t.memberExpression(objectRef, node.property, node.computed)), isArray ? t.arrayExpression([]) : t.objectExpression([]))))) 34 | } 35 | // else can't be ensured, just used the referenced expression 36 | return path.node 37 | } 38 | 39 | const safelyVisitors = { 40 | AssignmentExpression(path) { 41 | let { node } = path 42 | let { left, right } = node 43 | if (node.isSafe) { 44 | return 45 | } 46 | if (t.isMemberExpression(left)) { 47 | ensureObject(path.get('left').get('object'), t.isNumericLiteral(left.property)) 48 | markAsSafe(left) 49 | // else 50 | // expr == null ? void 0 : expr.property = right 51 | } 52 | 53 | }, 54 | MemberExpression(path) { 55 | let { node } = path 56 | if (node.isSafe) { 57 | return 58 | } 59 | let object = node.object 60 | let objectRef = object 61 | if (!t.isIdentifier(object)) { 62 | object = markAsSafe(t.assignmentExpression('=', getObjectReference(path), object)) 63 | objectRef = getObjectReference(path) 64 | } 65 | // object == null ? void 0 : object.property 66 | let safeMember = t.conditionalExpression( 67 | t.binaryExpression('==', object, t.nullLiteral()), 68 | t.unaryExpression('void', t.numericLiteral(0)), 69 | markAsSafe(t.memberExpression(objectRef, node.property, node.computed))) 70 | safeMember.isSafeMember = true 71 | path.replaceWith(safeMember) 72 | }, 73 | CallExpression: { 74 | enter(path) { 75 | let { node } = path 76 | if (node.isSafe) { 77 | return 78 | } 79 | let { callee } = node 80 | if (t.isMemberExpression(callee) && (callee.property.name === 'push' || callee.property.name === 'unshift')) { 81 | // for push or unshift, we ensure an array rather than doing existence checks 82 | if (ensureObject(path.get('callee').get('object'), true)) { 83 | path.get('callee').node.isSafe = true 84 | path.node.isSafe = true 85 | } 86 | } 87 | }, 88 | exit(path) { 89 | let { node } = path 90 | if (node.isSafe) { 91 | return 92 | } 93 | if (node.callee.isSafeMember) { 94 | // objectExpr.method == null ? void 0 : objectExpr.method ? objectExpr.method(args) : void 0 95 | let member = path.get('callee').get('alternate').node 96 | path.get('callee').get('alternate').replaceWith( 97 | t.conditionalExpression( 98 | member, 99 | markAsSafe(t.callExpression(member, node.arguments)), 100 | t.unaryExpression('void', t.numericLiteral(0)))) 101 | path.replaceWith(node.callee) 102 | } else { 103 | // func == null ? void 0 : func() 104 | if (t.isIdentifier(node.callee)) { 105 | path.replaceWith(t.conditionalExpression( 106 | node.callee, 107 | markAsSafe(t.callExpression(node.callee, node.arguments)), 108 | t.unaryExpression('void', t.numericLiteral(0)))) 109 | } else { 110 | let funcRef = getObjectReference(path.get('callee'), 'func') 111 | path.replaceWith(t.conditionalExpression( 112 | t.assignmentExpression('=', funcRef, node.callee), 113 | markAsSafe(t.callExpression(funcRef, node.arguments)), 114 | t.unaryExpression('void', t.numericLiteral(0)))) 115 | 116 | } 117 | } 118 | } 119 | } 120 | } 121 | 122 | return { 123 | visitor: { 124 | CallExpression(path) { 125 | let { node, scope } = path 126 | let firstArg = node.arguments[0] 127 | let callee = node.callee 128 | if (callee.name === 'safely') { 129 | path.traverse(safelyVisitors) 130 | if (path.parent.type === 'ArrowFunctionExpression' && path.parent.body && path.parent.body.body && path.parent.body.body[1] && path.parent.body.body[1].argument === node) { 131 | // the node can be moved in an arrow function 132 | path.parent.body.body[1].argument = node.arguments[0] 133 | } else { 134 | path.replaceWith(node.arguments[0]) 135 | } 136 | } 137 | } 138 | } 139 | }; 140 | } 141 | --------------------------------------------------------------------------------