diff --git a/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts b/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts index 6397524c34..00773ef0d4 100644 --- a/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts +++ b/packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts @@ -140,13 +140,6 @@ export const noUnsupportedSyntax = createRule({ report(node, `'new' expression`); }, - PrivateIdentifier(node) { - if (!directives.getEnclosingTypegpuFunction()) { - return; - } - report(node, 'private identifier'); - }, - Property(node) { if (!directives.getEnclosingTypegpuFunction()) { return; diff --git a/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts b/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts index c808e702d0..1e04b7d909 100644 --- a/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts +++ b/packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts @@ -8,6 +8,7 @@ describe('noUnsupportedSyntax', () => { "const fn = () => { 'use gpu'; const x = 1; }", "const fn = () => { 'use gpu'; const x = Struct({ prop: 1}); }", "const fn = () => { 'use gpu'; let x = 1; }", + "const cls = new (class { #priv = 1; fn = () => { 'use gpu'; const a = this.#priv; } } )()", ], invalid: [ { @@ -209,15 +210,6 @@ describe('noUnsupportedSyntax', () => { }, ], }, - { - code: "const fn = () => { 'use gpu'; obj.#buffer.$ = 1; }", - errors: [ - { - messageId: 'unexpected', - data: { snippet: '#buffer', syntax: 'private identifier' }, - }, - ], - }, { code: "const fn = () => { 'use gpu'; const obj = { [key]: 1 }; }", errors: [ diff --git a/packages/tinyest-for-wgsl/src/externals.ts b/packages/tinyest-for-wgsl/src/externals.ts index afc2119a66..479ba914d4 100644 --- a/packages/tinyest-for-wgsl/src/externals.ts +++ b/packages/tinyest-for-wgsl/src/externals.ts @@ -20,18 +20,26 @@ export function tryFindExternalChain(ctx: Context, node: JsNode): string | undef if (node.type === 'ThisExpression') { return 'this'; } - if (node.type === 'MemberExpression' && !node.computed && node.property.type === 'Identifier') { + if (node.type === 'MemberExpression' && !node.computed) { if (ctx.visitedNodes.has(node)) { return; } ctx.visitedNodes.add(node); - if (node.property.name === '$') { + let property; + if (node.property.type === 'Identifier' && node.property.name !== '$') { + property = node.property.name; + } else if (node.property.type === 'PrivateName') { + property = `#${node.property.id.name}`; + } else if (node.property.type === 'PrivateIdentifier') { + property = `#${node.property.name}`; + } else { return; } + const lhs = tryFindExternalChain(ctx, node.object); if (lhs) { - return `${lhs}.${node.property.name}`; + return `${lhs}.${property}`; } } } diff --git a/packages/tinyest-for-wgsl/tests/parsers.test.ts b/packages/tinyest-for-wgsl/tests/parsers.test.ts index 26bf2e91aa..d46ceb01f7 100644 --- a/packages/tinyest-for-wgsl/tests/parsers.test.ts +++ b/packages/tinyest-for-wgsl/tests/parsers.test.ts @@ -1,5 +1,5 @@ import babel from '@babel/parser'; -import type { Node } from '@babel/types'; +import type { ClassDeclaration, ClassProperty, Expression, Node } from '@babel/types'; import * as acorn from 'acorn'; import { describe, expect, it } from 'vitest'; import { transpileFn } from '../src/parsers.ts'; @@ -346,4 +346,33 @@ describe('transpileFn', () => { ); }), ); + + it( + 'handles private property access', + dualTest((p) => { + // `this.#v` is only valid inside a class body, so we parse a class and pluck out the arrow function. + const tree = p(` + class Foo { + #v = 0; + fn = () => { + const k = this.#v; + }; + } + `) as ClassDeclaration | acorn.Program; + const cls = (tree.type === 'Program' ? tree.body[0] : tree) as + | ClassDeclaration + | acorn.ClassDeclaration; + const props = cls.body.body; + const lastProp = props.at(-1) as ClassProperty | acorn.PropertyDefinition; + const fn = lastProp.value as Expression | acorn.Expression; + + const { externalNames } = transpileFn(fn); + + expect(externalNames).toMatchInlineSnapshot(` + Set { + "this.#v", + } + `); + }), + ); }); diff --git a/packages/typegpu/tests/externalPropAccess.test.ts b/packages/typegpu/tests/externalPropAccess.test.ts index 4d98cad270..322e89ad79 100644 --- a/packages/typegpu/tests/externalPropAccess.test.ts +++ b/packages/typegpu/tests/externalPropAccess.test.ts @@ -91,4 +91,63 @@ describe('external prop access', () => { }" `); }); + + it('supports private property access', () => { + class Cls { + #const = tgpu.const(d.u32, 1); + + fn = () => { + 'use gpu'; + const a = this.#const.$; + }; + } + + const cls = new Cls(); + + expect(tgpu.resolve([cls.fn])).toMatchInlineSnapshot(` + "const const_1: u32 = 1u; + + fn fn_1() { + const a = const_1; + }" + `); + }); + + it('supports private property access in anonymous class', () => { + const cls = new (class { + #const = tgpu.const(d.u32, 1); + + fn = () => { + 'use gpu'; + const a = this.#const.$; + }; + })(); + + expect(tgpu.resolve([cls.fn])).toMatchInlineSnapshot(` + "const const_1: u32 = 1u; + + fn fn_1() { + const a = const_1; + }" + `); + }); + + it('supports props containing #', () => { + const cls = new (class { + '#const' = tgpu.const(d.u32, 0); + + fn = () => { + 'use gpu'; + const a = this['#const'].$; + }; + })(); + + expect(tgpu.resolve([cls.fn])).toMatchInlineSnapshot(` + "const item: u32 = 0u; + + fn fn_1() { + const a = item; + }" + `); + }); }); diff --git a/packages/unplugin-typegpu/src/core/common.ts b/packages/unplugin-typegpu/src/core/common.ts index 18224fc2cd..827e5ce376 100644 --- a/packages/unplugin-typegpu/src/core/common.ts +++ b/packages/unplugin-typegpu/src/core/common.ts @@ -277,6 +277,15 @@ function extractLabelledExpression(path: NodePath): [string, NodePath]; + } else if ( + path.node.type === 'ClassPrivateProperty' && + path.node.value && + path.node.key.type === 'PrivateName' + ) { + // class Class { + // #key = value; + // } + return [path.node.key.id.name, path.get('value') as NodePath]; } } @@ -380,7 +389,7 @@ function tryFindIdentifier(node: t.Node): string | undefined { /** * Checks if `node` contains a label and a tgpu expression that could be named. - * If so, it calls the provided callback. Nodes selected for naming include: + * If so, it calls the provided callback. Nodes selected for naming include (but are not limited to): * * `let name = tgpu.bindGroupLayout({});` (VariableDeclarator) * @@ -485,6 +494,12 @@ export const functionVisitor: TraverseOptions = { ); }, + ClassPrivateProperty(path, state) { + performExpressionNaming(state, path, (pathToName, name) => + state.wrapInAutoName(pathToName, name), + ); + }, + AssignmentExpression: { exit(path, state) { const runtimeFn = operators[path.node.operator as keyof typeof operators]; diff --git a/packages/unplugin-typegpu/test/auto-naming.test.ts b/packages/unplugin-typegpu/test/auto-naming.test.ts index 8209f9f8fd..fa3c8f431d 100644 --- a/packages/unplugin-typegpu/test/auto-naming.test.ts +++ b/packages/unplugin-typegpu/test/auto-naming.test.ts @@ -368,6 +368,47 @@ describe('[BABEL] auto naming', () => { `); }); + it('works with class private properties', () => { + const code = `\ + class Foo { + #const = tgpu.const(d.u32, 1); + #buff; + + constructor() { + this.#buff = root.createUniform(d.u32); + } + } + `; + + expect(babelTransform(code, { autoNamingEnabled: true })).toMatchInlineSnapshot(` + "class Foo { + #const = /*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(tgpu.const(d.u32, 1), "const"); + #buff; + constructor() { + this.#buff = /*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(root.createUniform(d.u32), "buff"); + } + }" + `); + }); + + it('works with anonymous classes', () => { + const code = `\ + const cls = new (class { + myConst = tgpu.const(d.u32, 0); + #const = tgpu.const(d.u32, 1); + })(); + console.log(cls); + `; + + expect(babelTransform(code, { autoNamingEnabled: true })).toMatchInlineSnapshot(` + "const cls = new class { + myConst = /*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(tgpu.const(d.u32, 0), "myConst"); + #const = /*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(tgpu.const(d.u32, 1), "const"); + }(); + console.log(cls);" + `); + }); + it('works with object properties', () => { const code = `\ import { tgpu } from 'typegpu'; @@ -853,6 +894,52 @@ describe('[ROLLUP] auto naming', () => { `); }); + it('works with class private properties', async () => { + const code = `\ + class Foo { + #const = tgpu.const(d.u32, 1); + #buff; + + constructor() { + this.#buff = root.createUniform(d.u32); + } + } + console.log(Foo); + `; + + expect(await rollupTransform(code, { autoNamingEnabled: true })).toMatchInlineSnapshot(` + "class Foo { + #const = (/*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(tgpu.const(d.u32, 1), "const")); + #buff; + + constructor() { + this.#buff = (/*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(root.createUniform(d.u32), "buff")); + } + } + console.log(Foo); + " + `); + }); + + it('works with anonymous classes', async () => { + const code = `\ + const cls = new (class { + myConst = tgpu.const(d.u32, 0); + #const = tgpu.const(d.u32, 1); + })(); + console.log(cls); + `; + + expect(await rollupTransform(code, { autoNamingEnabled: true })).toMatchInlineSnapshot(` + "const cls = new (class { + myConst = (/*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(tgpu.const(d.u32, 0), "myConst")); + #const = (/*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(tgpu.const(d.u32, 1), "const")); + })(); + console.log(cls); + " + `); + }); + it('works with object properties', async () => { const code = `\ import { tgpu } from 'typegpu'; diff --git a/packages/unplugin-typegpu/test/nested-externals.test.ts b/packages/unplugin-typegpu/test/nested-externals.test.ts index d883723e2a..db1019d295 100644 --- a/packages/unplugin-typegpu/test/nested-externals.test.ts +++ b/packages/unplugin-typegpu/test/nested-externals.test.ts @@ -140,4 +140,34 @@ describe('externals gathering', () => { ); }); }); + + describe('private prop access', () => { + const code = `\ + import tgpu, { d } from 'typegpu'; + + const cls = new (class { + #const = tgpu.const(d.u32, 1); + + fn = () => { + 'use gpu'; + const a = this.#const.$; + }; + })(); + + console.log(cls);`; + + it('works for BABEL', () => { + expect(extractExternals(babelTransform(code))).toMatchInlineSnapshot(` + "{ + "this.#const": () => this.#const + }" + `); + }); + + it('works for ROLLUP', async () => { + expect(extractExternals(await rollupTransform(code))).toMatchInlineSnapshot( + `"{"this.#const":() => this.#const}"`, + ); + }); + }); });