Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add class.hasInstance proposal (Stage 1) support #13959

Closed
wants to merge 34 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
23a0380
feat: brand-check init
Nov 10, 2021
0e190fa
feat: update brank check test
Nov 13, 2021
54d8fbe
fix: add exception handing
Nov 13, 2021
e352fe1
feat: fixed basic list
Feb 16, 2022
e7b7f2b
feat: add return case with any statement
Feb 18, 2022
118f6a1
feat: add ast/spec.md
Feb 22, 2022
27cc74e
feat: add plugin class brand check
Mar 14, 2022
b49580c
fix: Remove unused comments
Mar 14, 2022
7bf5484
Merge branch 'main' into feat/classhasintance
Mar 14, 2022
dba8628
feat: update test case
Mar 22, 2022
8068917
feat: update mr
Mar 24, 2022
669c512
Merge branch 'main' into feat/classhasintance
Mar 24, 2022
c7d09ae
Merge branch 'fix/pr' into feat/classhasintance
Mar 24, 2022
b8666a3
feat: update yarn check
Mar 24, 2022
5df87fa
fixed: delete unuse comment
Mar 24, 2022
2070c6e
fixed: class decorators
Mar 24, 2022
61220d3
Merge branch 'main' into feat/classhasintance
Mar 28, 2022
173cb35
Update packages/babel-generator/src/generators/expressions.ts
YuriTu Mar 28, 2022
2f9e7f4
Merge branch 'feat/classhasintance' of github.com:YuriTu/babel into f…
Mar 28, 2022
a3a5b72
fixed: pkg type error
Mar 28, 2022
2cfdd98
fixed: adjust token consumption
Mar 28, 2022
8adfc8b
feat: update tokenizer hasinstance
Apr 11, 2022
c0c2599
feat: update comments test case
Apr 12, 2022
d58414d
having diffcult when adding case
Apr 15, 2022
168fa02
fix: resolve parser options name
Apr 15, 2022
4e73912
fix: update token comsumption time
Apr 18, 2022
966889d
fix: update generator test case
Apr 18, 2022
d32b606
fix: update arguments case
Apr 18, 2022
e8ca303
Update packages/babel-parser/src/parser/expression.js
YuriTu Apr 19, 2022
9bc5e71
Update packages/babel-parser/ast/spec.md
YuriTu Apr 19, 2022
f82f9fd
fix: remove unused expression and snap file
Apr 19, 2022
c32e0c7
Update packages/babel-parser/src/parser/expression.js
YuriTu Apr 19, 2022
8c4ba97
Merge branch 'feat/classhasintance' of github.com:YuriTu/babel into f…
Apr 19, 2022
c115075
fix: error message
Apr 19, 2022
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 packages/babel-generator/src/generators/expressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,13 @@ export function ModuleExpression(node: t.ModuleExpression) {
this.rightBrace();
}
}

export function ClassHasInstanceExpression(node: t.ClassHasInstanceExpression) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should ensure @babel/generator prints the code according to the AST:

Suggested change
export function ClassHasInstanceExpression(node: t.ClassHasInstanceExpression) {
export function ClassHasInstanceExpression(node: t.ClassHasInstanceExpression) {
this.word("class");
this.token(".");
this.word("hasInstance");
this.printInnerComments(node);

Please also add a test case to babel-generator/test/fixtures/class-brand-check:

class Range {
    equals(obj) {
        class.hasInstance(obj)
        class/* 1 */./* 2 */hasInstance/* 3 */(obj)
    }
}

this.word("class");
this.token(".");
this.word("hasInstance");
this.printInnerComments(node);
this.token("(");
this.print(node.instance);
this.token(")");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class Range {
equals(range) {
class.hasInstance(range);
class/* 1 */./* 2 */hasInstance/* 3 */(range);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"plugins": ["proposal-class-brand-check","transform-runtime"]
}
11 changes: 11 additions & 0 deletions packages/babel-generator/test/fixtures/class-brand-check/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Range {
equals(range) {
/* 1 */

/* 2 */

/* 3 */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The output here is incorrect. It seems to me the comments are registered as innerComments of the BodyStatement, it should be attached as the innerComments of ClassHasInstanceExpression / MetaProperty.

class.hasInstance(range);
class.hasInstance(range);
}
}
1 change: 1 addition & 0 deletions packages/babel-parser/ast/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ These are the core @babel/parser (babylon) AST node types.
- [ParenthesizedExpression](#parenthesizedexpression)
- [DoExpression](#doexpression)
- [ModuleExpression](#moduleexpression)
- [ClassHasInstanceExpression](#classhasinstanceexpression)
- [Template Literals](#template-literals)
- [TemplateLiteral](#templateliteral)
- [TaggedTemplateExpression](#taggedtemplateexpression)
Expand Down
3 changes: 3 additions & 0 deletions packages/babel-parser/src/parse-error/standard-errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ export default (_: typeof toParseErrorCredentials) => ({
InvalidEscapedReservedWord: _<{| reservedWord: string |}>(
({ reservedWord }) => `Escape sequence in keyword ${reservedWord}.`,
),
InvalidHasinstanceParameter: _(
"'hasInstance' expression must have exactly one parameter.",
),
InvalidIdentifier: _<{| identifierName: string |}>(
({ identifierName }) => `Invalid identifier ${identifierName}.`,
),
Expand Down
38 changes: 37 additions & 1 deletion packages/babel-parser/src/parser/expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,42 @@ export default class ExpressionParser extends LValParser {
return node;
}

parseClassOrClassHasInstanceExpression<T: N.Class>(
node: T,
isStatement: boolean,
): N.Class | N.ClassHasInstanceExpression {
this.next();
if (this.eat(tt.dot)) {
if (this.isContextual(tt._hasInstance)) {
this.expectPlugin("classBrandCheck");
return this.parseClassHasInstanceExpression(
node,
this.state.startPos,
this.state.startLoc,
);
} else {
throw this.raise(Errors.UnexpectedToken, { at: node });
}
YuriTu marked this conversation as resolved.
Show resolved Hide resolved
}
this.takeDecorators(node);
return this.parseClass(node, isStatement);
}

parseClassHasInstanceExpression(base, startPos, startLoc) {
this.next(); // eat `hasinstance`
this.expect(tt.parenL);
const node = this.startNodeAt(startPos, startLoc);
const args = this.parseCallExpressionArguments(tt.parenR);
if (args.length !== 1 || args[0].type === "SpreadElement") {
throw this.raise(Errors.InvalidHasinstanceParameter, {
at: this.state.lastTokStartLoc,
});
}
node.instance = args[0];

return this.finishNode(node, "ClassHasInstanceExpression");
}

toReferencedArguments(
node: N.CallExpression | N.OptionalCallExpression,
isParenthesizedExpr?: boolean,
Expand Down Expand Up @@ -1164,7 +1200,7 @@ export default class ExpressionParser extends LValParser {
case tt._class:
node = this.startNode();
this.takeDecorators(node);
return this.parseClass(node, false);
return this.parseClassOrClassHasInstanceExpression(node, false);

case tt._new:
return this.parseNewOrNewTarget();
Expand Down
7 changes: 4 additions & 3 deletions packages/babel-parser/src/parser/statement.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ export default class StatementParser extends ExpressionParser {

case tt._class:
if (context) this.unexpected();
return this.parseClass(node, true);
return this.parseClassOrClassHasInstanceExpression(node, true);

case tt._if:
return this.parseIfStatement(node);
Expand Down Expand Up @@ -1363,8 +1363,9 @@ export default class StatementParser extends ExpressionParser {
isStatement: /* T === ClassDeclaration */ boolean,
optionalId?: boolean,
): T {
this.next();
this.takeDecorators(node);
if (this.eat(tt._class)) {
this.takeDecorators(node);
}

// A class definition is always strict mode code.
const oldStrict = this.state.strict;
Expand Down
6 changes: 4 additions & 2 deletions packages/babel-parser/src/plugins/placeholders.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,10 @@ export default (superClass: Class<Parser>): Class<Parser> =>
): T {
const type = isStatement ? "ClassDeclaration" : "ClassExpression";

this.next();
this.takeDecorators(node);
if (this.eat(tt._class)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change we will incorrectly allow

class class A {}

because class is eaten twice.

I suggest we remove this.takeDecorators in parseClassOrClassHasInstanceExpression and remove this.next() above.

this.takeDecorators(node);
}

const oldStrict = this.state.strict;

const placeholder = this.parsePlaceholder("Identifier");
Expand Down
1 change: 1 addition & 0 deletions packages/babel-parser/src/tokenizer/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ export const tt: { [name: string]: TokenType } = {
_set: createKeywordLike("set", { startsExpr }),
_static: createKeywordLike("static", { startsExpr }),
_yield: createKeywordLike("yield", { startsExpr }),
_hasInstance: createKeywordLike("hasInstance", { startsExpr }),

// Flow and TypeScript Keywordlike
_asserts: createKeywordLike("asserts", { startsExpr }),
Expand Down
5 changes: 5 additions & 0 deletions packages/babel-parser/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,11 @@ export type ModuleExpression = NodeBase & {
body: Program,
};

export type ClassHasInstanceExpression = NodeBase & {
type: "ClassHasInstanceExpression",
instance: Expression,
};

// Patterns

// TypeScript access modifiers
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Foo {
bar(obj) {
class.hasInstance(obj)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: \"classBrandCheck\". (3:14)",
"plugins": []
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Foo {
bar() {
class.hasInstance()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"throws": "SyntaxError: 'hasInstance' expression must have exactly one parameter. (3:26)",
"plugins": ["classBrandCheck"]
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Foo {
bar(foo) {
class.hasInstance(...foo)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"throws": "SyntaxError: 'hasInstance' expression must have exactly one parameter. (3:32)",
"plugins": ["classBrandCheck"]
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Foo {
bar(foo,bar) {
class.hasInstance(foo,...bar)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"throws": "SyntaxError: 'hasInstance' expression must have exactly one parameter. (3:36)",
"plugins": ["classBrandCheck"]
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Foo {
bar(foo,bar) {
class.hasInstance(foo,bar)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"throws": "SyntaxError: 'hasInstance' expression must have exactly one parameter. (3:33)",
"plugins": ["classBrandCheck"]
}

1 change: 1 addition & 0 deletions packages/babel-parser/typings/babel-parser.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export type ParserPlugin =
| "bigInt"
| "classPrivateMethods"
| "classPrivateProperties"
| "classBrandCheck"
| "classProperties"
| "classStaticBlock" // Enabled by default
| "decimal"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
temp/
44 changes: 44 additions & 0 deletions packages/babel-plugin-proposal-class-brand-check/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@babel/plugin-proposal-class-brand-check",
"version": "7.16.0",
"description": "",
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
"directory": "packages/babel-plugin-proposal-class-brand-check"
},
"license": "MIT",
"publishConfig": {
"access": "public"
},
"main": "./lib/index.js",
"exports": {
".": "./lib/index.js",
"./package.json": "./package.json"
},
"keywords": [
"babel-plugin"
],
"dependencies": {
"@babel/helper-plugin-test-runner": "workspace:^",
"@babel/helper-plugin-utils": "workspace:^",
"@babel/parser": "workspace:^",
"@babel/traverse": "workspace:^",
"@babel/types": "workspace:^"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
},
"conditions": {
"BABEL_8_BREAKING": [
null,
{
"exports": null
}
]
},
"engines": {
"node": ">=13.0.0"
},
"author": "The Babel Team (https://babel.dev/team)"
}
100 changes: 100 additions & 0 deletions packages/babel-plugin-proposal-class-brand-check/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { declare } from "@babel/helper-plugin-utils";
import * as t from "@babel/types";
export default declare(api => {
api.assertVersion(7);

return {
name: "proposal-class-brand-check",
manipulateOptions(opts, parserOpts) {
parserOpts.plugins.push("classBrandCheck");
},
visitor: {
ClassHasInstanceExpression(path) {
const key = "set";
const node = path.node;
const instanceItem = node.instance;
if (!instanceItem) {
throw new Error("Expected 1 arguments, but got " + instanceItem);
}
let setID = path.scope.generateUidIdentifier(key);
path.findParent(path => {
if (path.type === "ClassDeclaration") {
// unique map
const uniqueID = path.scope.parent.globals[key];
if (uniqueID) {
setID = uniqueID;
} else {
const id = t.identifier(setID.name);
const weakSet = t.identifier("WeakSet");
const expression = t.newExpression(weakSet, []);
const declarator = t.variableDeclarator(id, expression);
path.insertBefore(t.variableDeclaration("var", [declarator]));
path.scope.parent.globals[key] = setID;

const body = path.node.body;
if (body.type === "ClassBody") {
const bodyList = body.body;
const consFlag = bodyList.some(
node => node.kind === "constructor",
);
if (!consFlag) {
const key = "constructor";
const body = t.blockStatement([], []);
const constructorMethod = t.classMethod(
key,
t.identifier(key),
[],
body,
false,
false,
false,
false,
);
bodyList.unshift(constructorMethod);
}

bodyList.forEach(node => {
if (node.kind === "constructor") {
const consBody = node.body;
const expressionList = consBody.body;

const memberExpression = t.memberExpression(
t.identifier(setID.name),
t.identifier("add"),
false,
);

const callExpression = t.callExpression(memberExpression, [
t.thisExpression(),
]);
const addExpression = t.expressionStatement(callExpression);

const returnFlag = expressionList.findIndex(
expression => expression.type === "ReturnStatement",
);
if (returnFlag === -1) {
expressionList.push(addExpression);
} else if (returnFlag === 0) {
expressionList.unshift(addExpression);
} else {
expressionList.splice(returnFlag, 0, addExpression);
}
}
});
}
}
}
});
const memberExpression = t.memberExpression(
t.identifier(setID.name),
t.identifier("has"),
false,
);
const callExpression = t.callExpression(memberExpression, [
instanceItem,
]);
path.replaceWith(callExpression);
},
},
};
});
Loading