Skip to content
Open
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
125 changes: 120 additions & 5 deletions src/friendly_errors/sketch_verifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { parse } from 'acorn';
import { simple as walk } from 'acorn-walk';
import * as constants from '../core/constants';
import { FES } from './fes';
import { strandsBuiltinFunctions as builtInGLSLFunctions } from '../strands/strands_builtins';
import { strandsAddedP5Globals } from '../strands/strands_api';

// List of functions to ignore as they either are meant to be re-defined or
// generate false positive outputs.
Expand Down Expand Up @@ -78,13 +80,105 @@ export const verifierUtils = {
// `lineOffset` here to correct them.
const lineOffset = -1;

function isStrandsBuilderCall(node) {
if (node.type !== 'CallExpression' || !node.arguments?.length) return false;

const callee = node.callee;
// buildFilterShader(fn), ...
if (callee.type === 'Identifier' && /^build\w*Shader$/.test(callee.name)) {
return true;
}

// baseFilterShader().modify(fn), ...
if (
callee.type === 'MemberExpression' &&
callee.property?.type === 'Identifier' &&
callee.property.name === 'modify' &&
callee.object?.type === 'CallExpression' &&
callee.object.callee?.type === 'Identifier' &&
/^base\w*Shader$/.test(callee.object.callee.name)
) {
return true;
}
return false;
}

function recordCallbackBody(arg, strandsFunctionNames, strandsBodyRanges) {
if (!arg) return;

// named: buildFilterShader(displaceColorsCallback)
if (arg.type === 'Identifier') {
strandsFunctionNames.add(arg.name);
return;
}

// inline: buildFilterShader(() => { … }) / function () { … }
if (
arg.type === 'FunctionExpression' ||
arg.type === 'ArrowFunctionExpression'
) {
if (arg.body?.type === 'BlockStatement') {
strandsBodyRanges.push([arg.body.start, arg.body.end]);
} else if (arg.start != null) {
strandsBodyRanges.push([arg.start, arg.end]);
}
}
}

try {
const ast = parse(code, {
ecmaVersion: 'latest',
sourceType: 'module',
locations: true // This helps us get the line number.
});

const strandsFunctionNames = new Set();
const strandsBodyRanges = [];

walk(ast, {
CallExpression(node) {
if (!isStrandsBuilderCall(node)) return;
recordCallbackBody(
node.arguments[0],
strandsFunctionNames,
strandsBodyRanges
);
}
});

// resolve named callbacks to body ranges
walk(ast, {
FunctionDeclaration(node) {
if (node.id && strandsFunctionNames.has(node.id.name) && node.body) {
strandsBodyRanges.push([node.body.start, node.body.end]);
}
},
VariableDeclarator(node) {
if (
node.id?.type === 'Identifier' &&
strandsFunctionNames.has(node.id.name) &&
node.init &&
(node.init.type === 'FunctionExpression' ||
node.init.type === 'ArrowFunctionExpression')
) {
const body = node.init.body;
if (body?.type === 'BlockStatement') {
strandsBodyRanges.push([body.start, body.end]);
} else {
strandsBodyRanges.push([node.init.start, node.init.end]);
}
}
}
});

function isInsideStrands(node) {
if (node.start == null) return false;
for (const [s, e] of strandsBodyRanges) {
if (node.start >= s && node.start < e) return true;
}
return false;
}

walk(ast, {
VariableDeclarator(node) {
if (node.id.type === 'Identifier') {
Expand All @@ -97,15 +191,17 @@ export const verifierUtils = {
: 'variables';
userDefinitions[category].push({
name: node.id.name,
line: node.loc.start.line + lineOffset
line: node.loc.start.line + lineOffset,
insideStrands: isInsideStrands(node)
});
}
},
FunctionDeclaration(node) {
if (node.id && node.id.type === 'Identifier') {
userDefinitions.functions.push({
name: node.id.name,
line: node.loc.start.line + lineOffset
line: node.loc.start.line + lineOffset,
insideStrands: isInsideStrands(node)
});
}
},
Expand All @@ -115,7 +211,8 @@ export const verifierUtils = {
if (node.id && node.id.type === 'Identifier') {
userDefinitions.variables.push({
name: node.id.name,
line: node.loc.start.line + lineOffset
line: node.loc.start.line + lineOffset,
insideStrands: isInsideStrands(node)
});
}
}
Expand Down Expand Up @@ -182,8 +279,13 @@ export const verifierUtils = {
)
);

for (let { name, line } of allDefinitions) {
if (!ignoreFunction.includes(name) && globalFunctions.has(name)) {
for (let { name, line, insideStrands } of allDefinitions) {
if (
!ignoreFunction.includes(name) &&
!insideStrands &&
globalFunctions.has(name) &&
!strandsAddedP5Globals.has(name)
) {
const message = generateFriendlyError(
FES.log`function`,
name,
Expand All @@ -194,6 +296,19 @@ export const verifierUtils = {
}
}

// strands/GLSL check
for (const { name, line, insideStrands } of allDefinitions) {
if (!insideStrands) continue;
if (Object.hasOwn(builtInGLSLFunctions, name)) {
const message = generateFriendlyError(
'function',
name,
line + 1
);
FES.log`${message}`();
return true;
}
}
return false;
},

Expand Down
6 changes: 6 additions & 0 deletions src/strands/strands_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import {
STRANDS_INTERNAL_NAME_PREFIX
} from './strands_names';

// Names that strands adds to p5.prototype but were not original p5 globals.
export const strandsAddedP5Globals = new Set();

const BUILTIN_GLOBAL_SPECS = {
width: { typeInfo: DataType.float1, get: p => p.width },
height: { typeInfo: DataType.float1, get: p => p.height },
Expand Down Expand Up @@ -472,6 +475,9 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
for (const [functionName, overrides] of Object.entries(
strandsBuiltinFunctions
)) {
if (!Object.hasOwn(fn, functionName)) {
strandsAddedP5Globals.add(functionName);
}
const isp5Function = overrides[0].isp5Function;
if (isp5Function) {
const originalFn = fn[functionName];
Expand Down
Loading