Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
70 changes: 68 additions & 2 deletions packages/eslint-plugin/src/rules/noUnsupportedSyntax.ts
Comment thread
vende11s marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,30 @@ export const noUnsupportedSyntax = createRule({
});
}

function validateFunctionParameters(
node:
| TSESTree.ArrowFunctionExpression
| TSESTree.FunctionExpression
| TSESTree.FunctionDeclaration,
) {
if (directives.getEnclosingTypegpuFunction() !== node) {
return;
}

for (const parameter of node.params) {
if (
parameter.type !== 'Identifier' &&
parameter.type !== 'AssignmentPattern' &&
parameter.type !== 'ObjectPattern'
) {
report(parameter, 'unsupported function parameter binding pattern');
}
}
}

return {
ArrowFunctionExpression(node) {
validateFunctionParameters(node);
if (directives.getDirectiveStack().at(-2)?.directives.includes('use gpu')) {
report(node, 'arrow function');
}
Expand All @@ -40,6 +62,12 @@ export const noUnsupportedSyntax = createRule({
if (!directives.getEnclosingTypegpuFunction()) {
return;
}

if (node.left.type === 'ArrayPattern') {
report(node.left, 'destructuring assignment');
return;
}

if (unsupportedAssignmentOps.includes(node.operator)) {
report(node, `assignment expression '${node.operator}'`);
}
Expand Down Expand Up @@ -104,12 +132,14 @@ export const noUnsupportedSyntax = createRule({
},

FunctionDeclaration(node) {
validateFunctionParameters(node);
if (directives.getDirectiveStack().at(-2)?.directives.includes('use gpu')) {
report(node, 'function declaration');
}
},

FunctionExpression(node) {
validateFunctionParameters(node);
if (directives.getDirectiveStack().at(-2)?.directives.includes('use gpu')) {
report(node, 'function expression');
}
Expand Down Expand Up @@ -209,8 +239,9 @@ export const noUnsupportedSyntax = createRule({
if (!directives.getEnclosingTypegpuFunction()) {
return;
}
if (node.id.type !== 'Identifier') {
report(node, 'variable declaration using destructuring');

if (node.id.type !== 'Identifier' && node.id.type !== 'ObjectPattern') {
report(node, 'unsupported variable binding pattern');
}
},

Expand All @@ -220,10 +251,45 @@ export const noUnsupportedSyntax = createRule({
}
report(node, 'yield expression');
},

ObjectPattern(node) {
const gpuFunction = directives.getEnclosingTypegpuFunction();
if (!gpuFunction) {
return;
}

const parent = node.parent;
// The outer pattern reports unsupported nested destructuring
if (parent.type === 'Property' && parent.parent.type === 'ObjectPattern') {
return;
}

const isBlockDeclaration =
parent.type === 'VariableDeclarator' &&
parent.parent.type === 'VariableDeclaration' &&
parent.parent.parent.type === 'BlockStatement';
const isFunctionParameter = parent === gpuFunction;

if ((isBlockDeclaration || isFunctionParameter) && isSupportedObjectBindingPattern(node)) {
return;
}

report(node, 'object destructuring');
},
};
}),
});

const unsupportedAssignmentOps = ['&&=', '**=', '||=', '??='];
const unsupportedBinaryOps = ['==', '!=', 'in', 'instanceof', '|>'];
const unsupportedUnaryOps = ['+', 'typeof', 'void', 'delete'];

function isSupportedObjectBindingPattern(pattern: TSESTree.ObjectPattern): boolean {
return pattern.properties.every(
(prop) =>
prop.type === 'Property' &&
!prop.computed &&
prop.key.type === 'Identifier' &&
prop.value.type === 'Identifier',
);
}
157 changes: 154 additions & 3 deletions packages/eslint-plugin/tests/rules/noUnsupportedSyntax.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ describe('noUnsupportedSyntax', () => {
"const cls = new (class { #priv = 1; fn = () => { 'use gpu'; const a = this.#priv; } } )()",
"const fn = () => { 'use gpu'; switch (x) { case 1: break; } }",
"const fn = () => { 'use gpu'; const obj = { [key]: 1 }; }",
"const fn = () => { 'use gpu'; const { a } = obj; }",
"const fn = () => { 'use gpu'; const { a, b: renamed } = obj; }",
"const fn = () => { 'use gpu'; let { a } = obj; }",
"const fn = () => { 'use gpu'; { const { a } = obj; } }",
"const fn = ({ a, b: renamed }) => { 'use gpu'; }",
"const fn = function({ a, b: renamed }) { 'use gpu'; }",
"function fn({ a, b: renamed }) { 'use gpu'; }",
'const fn = () => { const { nested: { a } } = obj; }',
'const fn = () => { let a = 0; for ({ a } of source) {} }',
],
invalid: [
{
Expand Down Expand Up @@ -290,11 +299,153 @@ describe('noUnsupportedSyntax', () => {
],
},
{
code: "const fn = () => { 'use gpu'; const { a } = obj; }",
code: "const fn = () => { 'use gpu'; const { nested: { a } } = obj; }",
errors: [
{
messageId: 'unexpected',
data: { snippet: '{ a } = obj', syntax: 'variable declaration using destructuring' },
data: {
snippet: '{ nested: { a } }',
syntax: 'object destructuring',
},
},
],
},
{
code: "const fn = () => { 'use gpu'; const { a = 1 } = obj; }",
errors: [
{
messageId: 'unexpected',
data: {
snippet: '{ a = 1 }',
syntax: 'object destructuring',
},
},
{
messageId: 'unexpected',
data: {
snippet: 'a = 1',
syntax: 'assignment pattern (default parameter)',
},
},
],
},
{
code: "const fn = () => { 'use gpu'; const { ...rest } = obj; }",
errors: [
{
messageId: 'unexpected',
data: {
snippet: '{ ...rest }',
syntax: 'object destructuring',
},
},
],
},
{
code: "const fn = () => { 'use gpu'; const { [key]: a } = obj; }",
errors: [
{
messageId: 'unexpected',
data: {
snippet: '{ [key]: a }',
syntax: 'object destructuring',
},
},
],
},
{
code: "const fn = () => { 'use gpu'; for (const { value } = source; value < 10;) {} }",
errors: [
{
messageId: 'unexpected',
data: {
snippet: '{ value }',
syntax: 'object destructuring',
},
},
],
},
{
code: "const fn = () => { 'use gpu'; for (const { value } of source) {} }",
errors: [
{
messageId: 'unexpected',
data: {
snippet: '{ value }',
syntax: 'object destructuring',
},
},
],
},
{
code: "const fn = () => { 'use gpu'; let a = 0; for ({ a } of source) {} }",
errors: [
{
messageId: 'unexpected',
data: {
snippet: '{ a }',
syntax: 'object destructuring',
},
},
],
},
{
code: "const fn = ([a]) => { 'use gpu'; }",
errors: [
{
messageId: 'unexpected',
data: {
snippet: '[a]',
syntax: 'unsupported function parameter binding pattern',
},
},
],
},
{
code: "function fn({ nested: { a } }) { 'use gpu'; }",
errors: [
{
messageId: 'unexpected',
data: {
snippet: '{ nested: { a } }',
syntax: 'object destructuring',
},
},
],
},
{
code: "const fn = () => { 'use gpu'; let a = 0; ({ a } = obj); }",
errors: [
{
messageId: 'unexpected',
data: {
snippet: '{ a }',
syntax: 'object destructuring',
},
},
],
},
{
code: "const fn = () => { 'use gpu'; let a = 0; return ({ nested: { a } } = obj); }",
errors: [
{
messageId: 'unexpected',
data: {
snippet: '{ nested: { a } }',
syntax: 'object destructuring',
},
},
],
},
{
code: "const fn = function(...args) { 'use gpu'; }",
errors: [
{
messageId: 'unexpected',
data: {
snippet: '...args',
syntax: 'unsupported function parameter binding pattern',
},
},
],
},
Expand All @@ -303,7 +454,7 @@ describe('noUnsupportedSyntax', () => {
errors: [
{
messageId: 'unexpected',
data: { snippet: '[a] = arr', syntax: 'variable declaration using destructuring' },
data: { snippet: '[a] = arr', syntax: 'unsupported variable binding pattern' },
},
],
},
Expand Down
29 changes: 29 additions & 0 deletions packages/tinyest-for-wgsl/src/bindingPatterns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type * as babel from '@babel/types';
import type * as acorn from 'acorn';
import { NodeTypeCatalog, type BindingPattern } from 'tinyest';

export function parseBindingPattern(node: babel.LVal | acorn.Pattern): BindingPattern {
if (node.type === 'Identifier') {
return node.name;
}

if (node.type !== 'ObjectPattern') {
throw new Error(`Unsupported binding pattern: ${node.type}`);
}

return [
NodeTypeCatalog.objectPattern,
node.properties.map((prop): [name: string, alias: string] => {
if (
(prop.type !== 'Property' && prop.type !== 'ObjectProperty') ||
prop.computed ||
prop.key.type !== 'Identifier' ||
prop.value.type !== 'Identifier'
) {
throw new Error('Only simple object destructuring is currently supported.');
}

return [prop.key.name, prop.value.name];
}),
];
}
23 changes: 4 additions & 19 deletions packages/tinyest-for-wgsl/src/functionParts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type * as babel from '@babel/types';
import type * as acorn from 'acorn';
import * as tinyest from 'tinyest';
import type { JsNode } from './types.ts';
import { parseBindingPattern } from './bindingPatterns.ts';

type FunctionNode =
| acorn.ArrowFunctionExpression
Expand Down Expand Up @@ -80,35 +81,19 @@ function validateFunction(functionNode: FunctionNode): void {
}
}

function parseParams(functionNode: FunctionNode): tinyest.FuncParameter[] {
function parseParams(functionNode: FunctionNode): tinyest.BindingPattern[] {
return (
functionNode.params as (
| babel.Identifier
| acorn.Identifier
| babel.ObjectPattern
| acorn.ObjectPattern
)[]
).map((param) =>
param.type === 'ObjectPattern'
? {
type: tinyest.FuncParameterType.destructuredObject,
props: param.properties.flatMap((prop) =>
(prop.type === /* acorn */ 'Property' || prop.type === /* babel */ 'ObjectProperty') &&
prop.key.type === 'Identifier' &&
prop.value.type === 'Identifier'
? [{ name: prop.key.name, alias: prop.value.name }]
: [],
),
}
: {
type: tinyest.FuncParameterType.identifier,
name: param.name,
},
);
).map(parseBindingPattern);
}

export function extractFunctionParts(rootNode: JsNode): {
params: tinyest.FuncParameter[];
params: tinyest.BindingPattern[];
body: acorn.BlockStatement | acorn.Expression | babel.BlockStatement | babel.Expression;
} {
const functionNode = unwrapToFunction(rootNode);
Expand Down
8 changes: 4 additions & 4 deletions packages/tinyest-for-wgsl/src/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ import { extractFunctionParts } from './functionParts.ts';

const { NodeTypeCatalog: NODE } = tinyest;

function createContext(params: tinyest.FuncParameter[], opts: TranspilationOptions): Context {
function createContext(params: tinyest.BindingPattern[], opts: TranspilationOptions): Context {
return {
externalNames: new Map(),
ignoreExternalDepth: 0,
visitedNodes: new Set(),
stack: [
{
declaredNames: params.flatMap((param) =>
param.type === tinyest.FuncParameterType.identifier
? param.name
: param.props.map((prop) => prop.alias),
tinyest.isObjectPattern(param)
? param[1].map(([, alias]) => alias)
: tinyest.extractIdentifierString(param),
),
},
],
Expand Down
Loading
Loading