Skip to content

Commit

Permalink
Merge pull request #1120 from Windvis/feature/support-dynamic-helpers
Browse files Browse the repository at this point in the history
Add support for the `helper` helper
  • Loading branch information
ef4 authored Feb 28, 2022
2 parents 920af41 + fbf17fa commit 2181bf8
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 1 deletion.
21 changes: 21 additions & 0 deletions packages/compat/src/resolver-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export function makeResolverTransform(resolver: Resolver) {
handleComponentHelper(node.params[0], resolver, filename, scopeStack);
return;
}
if (node.path.original === 'helper' && node.params.length > 0) {
handleDynamicHelper(node.params[0], resolver, filename);
return;
}
resolver.resolveSubExpression(node.path.original, filename, node.path.loc);
},
MustacheStatement(node: ASTv1.MustacheStatement) {
Expand All @@ -97,6 +101,10 @@ export function makeResolverTransform(resolver: Resolver) {
handleComponentHelper(node.params[0], resolver, filename, scopeStack);
return;
}
if (node.path.original === 'helper' && node.params.length > 0) {
handleDynamicHelper(node.params[0], resolver, filename);
return;
}
let hasArgs = node.params.length > 0 || node.hash.pairs.length > 0;
let resolution = resolver.resolveMustache(node.path.original, hasArgs, filename, node.path.loc);
if (resolution && resolution.type === 'component') {
Expand Down Expand Up @@ -320,3 +328,16 @@ function handleComponentHelper(

resolver.resolveComponentHelper(locator, moduleName, param.loc, impliedBecause);
}

function handleDynamicHelper(param: ASTv1.Node, resolver: Resolver, moduleName: string): void {
switch (param.type) {
case 'StringLiteral':
resolver.resolveDynamicHelper({ type: 'literal', path: param.value }, moduleName, param.loc);
break;
case 'TextNode':
resolver.resolveDynamicHelper({ type: 'literal', path: param.chars }, moduleName, param.loc);
break;
default:
resolver.resolveDynamicHelper({ type: 'other' }, moduleName, param.loc);
}
}
40 changes: 39 additions & 1 deletion packages/compat/src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,14 @@ const builtInHelpers = [
'hasBlock',
'hasBlockParams',
'hash',
'helper',
'if',
'input',
'let',
'link-to',
'loc',
'log',
// 'modifier',
'mount',
'mut',
'on',
Expand All @@ -108,7 +110,6 @@ const builtInHelpers = [
];

const builtInComponents = ['input', 'link-to', 'textarea'];

const builtInModifiers = ['action', 'on'];

// this is a subset of the full Options. We care about serializability, and we
Expand Down Expand Up @@ -821,6 +822,43 @@ export default class CompatResolver implements Resolver {
from
);
}

resolveDynamicHelper(helper: ComponentLocator, from: string, loc: Loc): Resolution | null {
if (!this.staticHelpersEnabled) {
return null;
}

if (helper.type === 'literal') {
let helperName = helper.path;
if (builtInHelpers.includes(helperName)) {
return null;
}

let found = this.tryHelper(helperName, from);
if (found) {
return this.add(found, from);
}
return this.add(
{
type: 'error',
message: `Missing helper`,
detail: helperName,
loc,
},
from
);
} else {
return this.add(
{
type: 'error',
message: 'Unsafe dynamic helper',
detail: `cannot statically analyze this expression`,
loc,
},
from
);
}
}
}

function humanReadableFile(root: string, file: string) {
Expand Down
70 changes: 70 additions & 0 deletions packages/compat/tests/resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,18 @@ describe('compat-resolver', function () {
},
]);
});
test('string literal passed to `helper` helper in content position', function () {
let findDependencies = configure({
staticHelpers: true,
});
givenFile('helpers/hello-world.js');
expect(findDependencies('templates/application.hbs', `{{helper "hello-world"}}`)).toEqual([
{
path: '../helpers/hello-world.js',
runtimeName: 'the-app/helpers/hello-world',
},
]);
});
test('built-in components are ignored when used with the component helper', function () {
let findDependencies = configure({
staticComponents: true,
Expand All @@ -534,6 +546,21 @@ describe('compat-resolver', function () {
)
).toEqual([]);
});
test('built-in helpers are ignored when used with the "helper" helper', function () {
let findDependencies = configure({
staticHelpers: true,
});
expect(
findDependencies(
'templates/application.hbs',
`
{{helper "fn"}}
{{helper "array"}}
{{helper "concat"}}
`
)
).toEqual([]);
});
test('component helper with direct addon package reference', function () {
let findDependencies = configure({
staticComponents: true,
Expand Down Expand Up @@ -635,18 +662,47 @@ describe('compat-resolver', function () {
},
]);
});
test('string literal passed to "helper" helper in helper position', function () {
let findDependencies = configure({ staticHelpers: true });
givenFile('helpers/hello-world.js');
expect(
findDependencies(
'templates/application.hbs',
`
{{#let (helper "hello-world") as |helloWorld|}}
{{helloWorld}}
{{/let}}
`
)
).toEqual([
{
path: '../helpers/hello-world.js',
runtimeName: 'the-app/helpers/hello-world',
},
]);
});
test('string literal passed to component helper fails to resolve', function () {
let findDependencies = configure({ staticComponents: true });
givenFile('components/my-thing.js');
expect(() => {
findDependencies('templates/application.hbs', `{{my-thing header=(component "hello-world") }}`);
}).toThrow(new RegExp(`Missing component: hello-world in templates/application.hbs`));
});
test('string literal passed to "helper" helper fails to resolve', function () {
let findDependencies = configure({ staticHelpers: true });
expect(() => {
findDependencies('templates/application.hbs', `{{helper "hello-world"}}`);
}).toThrow(new RegExp(`Missing helper: hello-world in templates/application.hbs`));
});
test('string literal passed to component helper fails to resolve when staticComponents is off', function () {
let findDependencies = configure({ staticComponents: false });
givenFile('components/my-thing.js');
expect(findDependencies('templates/application.hbs', `{{my-thing header=(component "hello-world") }}`)).toEqual([]);
});
test('string literal passed to "helper" helper fails to resolve when staticHelpers is off', function () {
let findDependencies = configure({ staticHelpers: false });
expect(findDependencies('templates/application.hbs', `{{helper "hello-world"}}`)).toEqual([]);
});
test('dynamic component helper error in content position', function () {
let findDependencies = configure({ staticComponents: true });
givenFile('components/hello-world.js');
Expand Down Expand Up @@ -1708,6 +1764,20 @@ describe('compat-resolver', function () {
);
});

test('rejects arbitrary expression in "helper" helper', function () {
let findDependencies = configure({ staticHelpers: true });
expect(() => findDependencies('templates/application.hbs', `{{helper (some-helper this.which) }}`)).toThrow(
`Unsafe dynamic helper: cannot statically analyze this expression`
);
});

test('rejects any non-string-literal in "helper" helper', function () {
let findDependencies = configure({ staticHelpers: true });
expect(() => findDependencies('templates/application.hbs', `{{helper this.which }}`)).toThrow(
`Unsafe dynamic helper: cannot statically analyze this expression`
);
});

test('trusts inline ensure-safe-component helper', function () {
let findDependencies = configure({ staticComponents: true });
expect(findDependencies('templates/application.hbs', `{{component (ensure-safe-component this.which) }}`)).toEqual(
Expand Down

0 comments on commit 2181bf8

Please sign in to comment.