Skip to content

Commit

Permalink
Add transform support for optional chaining assignment
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolo-ribaudo committed Jul 5, 2023
1 parent 5933ab1 commit 67a2c5e
Show file tree
Hide file tree
Showing 56 changed files with 514 additions and 15 deletions.
6 changes: 6 additions & 0 deletions packages/babel-helpers/src/helpers.ts
Expand Up @@ -1884,3 +1884,9 @@ helpers.identity = helper("7.17.0")`
return x;
}
`;

helpers.nullishReceiverError = helper("7.22.6")`
export default function _nullishReceiverError(r) {
throw new TypeError("Cannot set property of null or undefined.");
}
`;
@@ -0,0 +1,3 @@
src
test
*.log
19 changes: 19 additions & 0 deletions packages/babel-plugin-proposal-optional-chaining-assign/README.md
@@ -0,0 +1,19 @@
# @babel/plugin-transform-optional-chaining

> Transform optional chaining operators into a series of nil checks
See our website [@babel/plugin-transform-optional-chaining](https://babeljs.io/docs/babel-plugin-transform-optional-chaining) for more information.

## Install

Using npm:

```sh
npm install --save-dev @babel/plugin-transform-optional-chaining
```

or using yarn:

```sh
yarn add @babel/plugin-transform-optional-chaining --dev
```
@@ -0,0 +1,55 @@
{
"name": "@babel/plugin-proposal-optional-chaining-assign",
"version": "7.22.5",
"description": "Transform optional chaining on the left-hand side of assignment expressions",
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
"directory": "packages/babel-plugin-proposal-optional-chaining-assign"
},
"homepage": "https://babel.dev/docs/en/next/babel-plugin-proposal-optional-chaining-assign",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"main": "./lib/index.js",
"keywords": [
"babel-plugin"
],
"dependencies": {
"@babel/helper-plugin-utils": "workspace:^",
"@babel/helper-skip-transparent-expression-wrappers": "workspace:^",
"@babel/plugin-syntax-optional-chaining-assign": "workspace:^",
"@babel/plugin-transform-optional-chaining": "workspace:^"
},
"peerDependencies": {
"@babel/core": "^7.22.5"
},
"devDependencies": {
"@babel/helper-plugin-test-runner": "workspace:^"
},
"engines": {
"node": ">=6.9.0"
},
"author": "The Babel Team (https://babel.dev/team)",
"conditions": {
"BABEL_8_BREAKING": [
{
"engines": {
"node": "^16.20.0 || ^18.16.0 || >=20.0.0"
}
}
],
"USE_ESM": [
{
"type": "module"
},
null
]
},
"exports": {
".": "./lib/index.js",
"./package.json": "./package.json"
},
"type": "commonjs"
}
@@ -0,0 +1,53 @@
import { declare } from "@babel/helper-plugin-utils";
import syntaxOptionalChainingAssign from "@babel/plugin-syntax-optional-chaining-assign";
import type { NodePath } from "@babel/traverse";
import type * as t from "@babel/types";
import { skipTransparentExprWrappers } from "@babel/helper-skip-transparent-expression-wrappers";
import { transformOptionalChain } from "@babel/plugin-transform-optional-chaining";

export default declare(api => {
api.assertVersion("^7.22.5");

const assumptions = {
noDocumentAll: api.assumption("noDocumentAll") ?? false,
pureGetters: api.assumption("pureGetters") ?? false,
};

const { types: t } = api;

return {
name: "transform-optional-chaining-assign",
inherits: syntaxOptionalChainingAssign,

visitor: {
AssignmentExpression(path, state) {
let lhs = path.get("left");
if (!lhs.isExpression()) return;
const isParenthesized =
lhs.node.extra?.parenthesized ||
t.isParenthesizedExpression(lhs.node);

lhs = skipTransparentExprWrappers(lhs) as NodePath<
t.LVal & t.Expression
>;
if (!lhs.isOptionalMemberExpression()) return;

let ifNullish: t.Expression = path.scope.buildUndefinedNode();
if (isParenthesized) {
ifNullish = t.callExpression(
state.addHelper("nullishReceiverError"),
[],
);
if (path.node.operator === "=") {
ifNullish = t.sequenceExpression([
t.cloneNode(path.node.right),
ifNullish,
]);
}
}

transformOptionalChain(lhs, assumptions, path, ifNullish);
},
},
};
});
@@ -0,0 +1 @@
a.b?.c.d?.e.f = g;
@@ -0,0 +1,2 @@
var _a$b;
(_a$b = a.b) == null || (_a$b = _a$b.c.d) == null ? void 0 : _a$b.e.f = g;
@@ -0,0 +1 @@
a.b?.c.d?.().e.f = g;
@@ -0,0 +1,2 @@
var _a$b, _a$b$c$d, _a$b$c;
(_a$b = a.b) == null || (_a$b$c$d = (_a$b$c = _a$b.c).d) == null ? void 0 : _a$b$c$d.call(_a$b$c).e.f = g;
@@ -0,0 +1,6 @@
{
"assumptions": {
"noDocumentAll": true
},
"plugins": ["proposal-optional-chaining-assign"]
}
@@ -0,0 +1,13 @@
let a = null;

let evaluated = false;
expect(() => {
(a?.b) = (evaluated = true);
}).toThrow(TypeError);
//expect(evaluated).toBe(true);

evaluated = false;
expect(() => {
(a?.b) += (evaluated = true);
}).toThrow(TypeError);
expect(evaluated).toBe(false);
@@ -0,0 +1,2 @@
(a?.b) = c;
(a?.b) += c;
@@ -0,0 +1,3 @@
var _a, _a2;
(_a = a) == null ? (c, babelHelpers.nullishReceiverError()) : _a.b = c;
(_a2 = a) == null ? babelHelpers.nullishReceiverError() : _a2.b += c;
@@ -0,0 +1 @@
a.b?.c.d?.().e.f = g;
@@ -0,0 +1,2 @@
var _a$b;
(_a$b = a.b) === null || _a$b === void 0 || _a$b.c.d === null || _a$b.c.d === void 0 ? void 0 : _a$b.c.d().e.f = g;
@@ -0,0 +1,6 @@
{
"assumptions": {
"pureGetters": true
},
"plugins": ["proposal-optional-chaining-assign"]
}
@@ -0,0 +1,38 @@
let a = null;

function never(i) {
throw new Error("This should not be evaluated " + i);
}

expect(() => {
a.b?.c.d?.e.f = never(1);
}).toThrow(TypeError);

a = { b: null };
expect(() => {
a.b?.c.d?.e.f = never(2);
}).not.toThrow();

a = { b: { c: null } };
expect(() => {
a.b?.c.d?.e.f = never(3);
}).toThrow(TypeError);

a = { b: { c: { d: null } } };
expect(() => {
a.b?.c.d?.e.f = never(4);
}).not.toThrow();

a = { b: { c: { d: { e: null } } } };
let evaluated = false;
expect(() => {
a.b?.c.d?.e.f = (evaluated = true);
}).toThrow(TypeError);
expect(evaluated).toBe(true);

a = { b: { c: { d: { e: {} } } } };
let g = {};
expect(() => {
a.b?.c.d?.e.f = g;
}).not.toThrow();
expect(a.b.c.d.e.f).toBe(g);
@@ -0,0 +1 @@
a.b?.c.d?.e.f = g;
@@ -0,0 +1,2 @@
var _a$b;
(_a$b = a.b) === null || _a$b === void 0 || (_a$b = _a$b.c.d) === null || _a$b === void 0 ? void 0 : _a$b.e.f = g;
@@ -0,0 +1 @@
a.b?.c.d?.().e.f = g;
@@ -0,0 +1,2 @@
var _a$b, _a$b$c$d, _a$b$c;
(_a$b = a.b) === null || _a$b === void 0 || (_a$b$c$d = (_a$b$c = _a$b.c).d) === null || _a$b$c$d === void 0 ? void 0 : _a$b$c$d.call(_a$b$c).e.f = g;
@@ -0,0 +1,13 @@
let a = null;

let evaluated = false;
expect(() => {
(a?.b) = (evaluated = true);
}).toThrow(TypeError);
//expect(evaluated).toBe(true);

evaluated = false;
expect(() => {
(a?.b) += (evaluated = true);
}).toThrow(TypeError);
expect(evaluated).toBe(false);
@@ -0,0 +1,2 @@
(a?.b) = c;
(a?.b) += c;
@@ -0,0 +1,3 @@
var _a, _a2;
(_a = a) === null || _a === void 0 ? (c, babelHelpers.nullishReceiverError()) : _a.b = c;
(_a2 = a) === null || _a2 === void 0 ? babelHelpers.nullishReceiverError() : _a2.b += c;
@@ -0,0 +1,6 @@
let obj = null;
expect(obj?.x += 3).toBe(undefined);

obj = { x: 1 };
expect(obj?.x += 3).toBe(4);
expect(obj.x).toBe(4);
@@ -0,0 +1 @@
a?.b.c?.d.e += 3;
@@ -0,0 +1,2 @@
var _a;
(_a = a) === null || _a === void 0 || (_a = _a.b.c) === null || _a === void 0 ? void 0 : _a.d.e += 3;
@@ -0,0 +1 @@
a?.() = b;
@@ -0,0 +1,3 @@
{
"throws": "a"
}
@@ -0,0 +1 @@
++a?.b;
@@ -0,0 +1,3 @@
{
"throws": "a"
}
@@ -0,0 +1 @@
++a?.b;
@@ -0,0 +1,3 @@
{
"throws": "a"
}
@@ -0,0 +1,3 @@
{
"plugins": ["proposal-optional-chaining-assign"]
}
@@ -0,0 +1,3 @@
import runner from "@babel/helper-plugin-test-runner";

runner(import.meta.url);
@@ -0,0 +1 @@
{ "type": "module" }

0 comments on commit 67a2c5e

Please sign in to comment.