From 44926472e748beb199327182308062f5d664b1a2 Mon Sep 17 00:00:00 2001 From: Aryaneefds Date: Sat, 12 Sep 2026 17:18:48 +0530 Subject: [PATCH] Preserve element type through sort/sort_natural filters Fixes #1086: media | sort lost its type (untyped[]) because the docset declares sort as returning an untyped array. Infer the input type instead: arrays keep their value type, single values wrap into a single-element array type, unknown input falls back to docset. --- .../src/TypeSystem.spec.ts | 55 +++++++++++++++++++ .../src/TypeSystem.ts | 15 +++++ 2 files changed, 70 insertions(+) diff --git a/packages/theme-language-server-common/src/TypeSystem.spec.ts b/packages/theme-language-server-common/src/TypeSystem.spec.ts index 10b297f62..2045fa94a 100644 --- a/packages/theme-language-server-common/src/TypeSystem.spec.ts +++ b/packages/theme-language-server-common/src/TypeSystem.spec.ts @@ -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 () => ({}), }, @@ -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 }}`); diff --git a/packages/theme-language-server-common/src/TypeSystem.ts b/packages/theme-language-server-common/src/TypeSystem.ts index 9b8859d3c..9a9e06832 100644 --- a/packages/theme-language-server-common/src/TypeSystem.ts +++ b/packages/theme-language-server-common/src/TypeSystem.ts @@ -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 {