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
55 changes: 55 additions & 0 deletions packages/theme-language-server-common/src/TypeSystem.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,16 @@ describe('Module: TypeSystem', () => {
name: 'size',
return_type: [{ type: 'number', name: '' }],
},
{
// The docset declares sort as returning untyped[].
// See https://github.com/Shopify/theme-tools/issues/1086
name: 'sort',
return_type: [{ type: 'array', array_value: 'untyped' }],
},
{
name: 'sort_natural',
return_type: [{ type: 'array', array_value: 'untyped' }],
},
],
systemTranslations: async () => ({}),
},
Expand Down Expand Up @@ -259,6 +269,51 @@ describe('Module: TypeSystem', () => {
expect(inferredType).to.equal('number');
});

describe('when using the sort filter', () => {
// https://github.com/Shopify/theme-tools/issues/1086
it('should preserve the array value type', async () => {
const ast = toLiquidHtmlAST(`{% assign x = all_products | sort %}`);
const xVariable = (ast as any).children[0].markup as AssignMarkup;
const inferredType = await typeSystem.inferType(xVariable, ast, 'file:///file.liquid');
expect(inferredType).to.eql({ kind: 'array', valueType: 'product' });
});

it('should wrap a single value type into an array', async () => {
const ast = toLiquidHtmlAST(`{% assign x = product | sort %}`);
const xVariable = (ast as any).children[0].markup as AssignMarkup;
const inferredType = await typeSystem.inferType(xVariable, ast, 'file:///file.liquid');
expect(inferredType).to.eql({ kind: 'array', valueType: 'product' });
});

it('should preserve the array value type for sort_natural', async () => {
const ast = toLiquidHtmlAST(`{% assign x = all_products | sort_natural %}`);
const xVariable = (ast as any).children[0].markup as AssignMarkup;
const inferredType = await typeSystem.inferType(xVariable, ast, 'file:///file.liquid');
expect(inferredType).to.eql({ kind: 'array', valueType: 'product' });
});

it('should preserve the type through a filter chain ending in sort', async () => {
const ast = toLiquidHtmlAST(`{% assign x = all_products | sort | sort %}`);
const xVariable = (ast as any).children[0].markup as AssignMarkup;
const inferredType = await typeSystem.inferType(xVariable, ast, 'file:///file.liquid');
expect(inferredType).to.eql({ kind: 'array', valueType: 'product' });
});

it('should preserve the lookup type for untyped properties', async () => {
const ast = toLiquidHtmlAST(`{% assign x = product.metafields | sort %}`);
const xVariable = (ast as any).children[0].markup as AssignMarkup;
const inferredType = await typeSystem.inferType(xVariable, ast, 'file:///file.liquid');
expect(inferredType).to.eql({ kind: 'array', valueType: 'product_metafields' });
});

it('should fall back to the docset type for unknown input', async () => {
const ast = toLiquidHtmlAST(`{% assign x = unknown_variable | sort %}`);
const xVariable = (ast as any).children[0].markup as AssignMarkup;
const inferredType = await typeSystem.inferType(xVariable, ast, 'file:///file.liquid');
expect(inferredType).to.eql({ kind: 'array', valueType: 'untyped' });
});
});

describe('when using string builtin methods', () => {
it('should return number for size', async () => {
const ast = toLiquidHtmlAST(`{{ product.title.size }}`);
Expand Down
15 changes: 15 additions & 0 deletions packages/theme-language-server-common/src/TypeSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,21 @@ function inferType(
return inferType(lastFilter.args[0], symbolsTable, objectMap, filtersMap);
}
}
if (lastFilter.name === 'sort' || lastFilter.name === 'sort_natural') {
// sort/sort_natural preserve the element type: an array input keeps its
// value type and a single value becomes a single-element array of that
// type (a common trick to coerce a drop into an array). The docset
// declares these filters as returning untyped[], which loses the type.
// See https://github.com/Shopify/theme-tools/issues/1086
const inputVariable = { ...thing, filters: thing.filters.slice(0, -1) };
const inputType = inferType(inputVariable, symbolsTable, objectMap, filtersMap);
if (isArrayType(inputType)) {
return inputType;
}
if (inputType !== Untyped && inputType !== Unknown) {
return arrayType(inputType);
}
}
const filterEntry = filtersMap[lastFilter.name];
return filterEntry ? filterEntryReturnType(filterEntry) : Untyped;
} else {
Expand Down
Loading