UNPKG

@shopify/theme-language-server-common

Version:

<h1 align="center" style="position: relative;" > <br> <img src="https://github.com/Shopify/theme-check-vscode/blob/main/images/shopify_glyph.png?raw=true" alt="logo" width="141" height="160"> <br> Theme Language Server </h1>

345 lines (328 loc) 11.4 kB
import { AssignMarkup, LiquidVariableOutput, NamedTags, NodeTypes, toLiquidHtmlAST, } from '@shopify/liquid-html-parser'; import { path as pathUtils } from '@shopify/theme-check-common'; import { assert, beforeEach, describe, expect, it, vi } from 'vitest'; import { URI } from 'vscode-uri'; import { SettingsSchemaJSONFile } from './settings'; import { ArrayType, TypeSystem } from './TypeSystem'; import { isLiquidVariableOutput, isNamedLiquidTag } from './utils'; describe('Module: TypeSystem', () => { let typeSystem: TypeSystem; let settingsProvider: any; const literalContexts = [ { value: `10`, type: 'number' }, { value: `'string'`, type: 'string' }, { value: `true`, type: 'boolean' }, // { value: `null`, type: 'untyped' }, ]; beforeEach(() => { settingsProvider = vi.fn().mockResolvedValue([]); typeSystem = new TypeSystem( { tags: async () => [], objects: async () => [ { name: 'all_products', return_type: [{ type: 'array', array_value: 'product' }], }, { name: 'product', access: { global: false, parents: [], template: [], }, return_type: [], properties: [ { name: 'featured_image', description: 'ze best image for ze product', return_type: [{ type: 'image', name: '' }], }, ], }, { name: 'settings', return_type: [], properties: [], // these should be populated dynamically }, { name: 'predictive_search', access: { global: false, parents: [], template: [] }, return_type: [], }, { name: 'comment', access: { global: false, parents: [], template: [] }, return_type: [], }, { name: 'recommendations', access: { global: false, parents: [], template: [] }, return_type: [], }, { name: 'app', access: { global: false, parents: [], template: [] }, return_type: [], }, { name: 'section', access: { global: false, parents: [], template: [] }, return_type: [], properties: [ { name: 'settings', return_type: [{ type: 'untyped', name: '' }], }, ], }, { name: 'block', access: { global: false, parents: [], template: [] }, return_type: [], properties: [ { name: 'settings', return_type: [{ type: 'untyped', name: '' }], }, ], }, { name: 'locale', access: { global: false, parents: [], template: [] }, return_type: [], }, ], filters: async () => [ { name: 'size', return_type: [{ type: 'number', name: '' }], }, ], systemTranslations: async () => ({}), }, settingsProvider, ); }); it('should return the type of assign markup nodes (basic test)', async () => { for (const { value, type } of literalContexts) { const ast = toLiquidHtmlAST(`{% assign x = ${value} %}`); const assignMarkup = (ast as any).children[0].markup as AssignMarkup; const inferredType = await typeSystem.inferType(assignMarkup, ast, 'file:///file.liquid'); expect(inferredType, value).to.equal(type); } }); it('should return the type of other variables', async () => { for (const { value, type } of literalContexts) { const ast = toLiquidHtmlAST(`{% assign x = ${value} %}{% assign y = x %}`); const yVariable = (ast as any).children[1].markup as AssignMarkup; const inferredType = await typeSystem.inferType(yVariable, ast, 'file:///file.liquid'); expect(inferredType).to.equal(type); } }); it('should return the type of expressions', async () => { for (const { value, type } of literalContexts) { const ast = toLiquidHtmlAST(`{{ ${value} }}`); const output = ast.children[0] as LiquidVariableOutput; const variable = output.markup; if (typeof variable === 'string') throw new Error('expecting real deal'); const expression = variable.expression; const inferredType = await typeSystem.inferType(expression, ast, 'file:///file.liquid'); expect(inferredType, value).to.equal(type); } }); it('should return the type of array variables', async () => { const ast = toLiquidHtmlAST(`{% assign x = all_products %}`); 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 return the type of object properties', async () => { const ast = toLiquidHtmlAST(`{% assign x = all_products[0].featured_image %}`); const xVariable = (ast as any).children[0].markup as AssignMarkup; const inferredType = await typeSystem.inferType(xVariable, ast, 'file:///file.liquid'); expect(inferredType).to.equal('image'); }); it('should return the type of filtered variables', async () => { const ast = toLiquidHtmlAST(`{% assign x = all_products | size %}`); const xVariable = (ast as any).children[0].markup as AssignMarkup; const inferredType = await typeSystem.inferType(xVariable, ast, 'file:///file.liquid'); expect(inferredType).to.equal('number'); }); it('should return the type of variables in for loop', async () => { const ast = toLiquidHtmlAST(`{% for item in all_products %}{{ item }}{% endfor %}`); const forLoop = ast.children[0]; assert(isNamedLiquidTag(forLoop, NamedTags.for) && forLoop.children?.length === 1); const branch = forLoop.children[0]; assert(branch.type === NodeTypes.LiquidBranch); const variableOutput = branch.children[0]; assert(isLiquidVariableOutput(variableOutput)); const variable = variableOutput.markup; const inferredType = await typeSystem.inferType(variable, ast, 'file:///file.liquid'); expect(inferredType).to.equal('product'); }); it('should patch the properties of settings when a schema is available', async () => { settingsProvider.mockResolvedValue([ { name: 'category', settings: [ { id: 'slide', label: 'Slide label', type: 'checkbox', }, { id: 'my_font', label: 'my font', type: 'font_picker', }, ], }, ] as SettingsSchemaJSONFile); const contexts = [ { id: 'slide', expectedType: 'boolean' }, { id: 'my_font', expectedType: 'font' }, ]; for (const { id, expectedType } of contexts) { const ast = toLiquidHtmlAST(`{{ settings.${id} }}`); const variableOutput = ast.children[0]; assert(isLiquidVariableOutput(variableOutput)); const inferredType = await typeSystem.inferType( variableOutput.markup, ast, 'file:///file.liquid', ); expect(inferredType).to.eql(expectedType); } }); it('should support section settings in section files', async () => { const sourceCode = ` {{ section.settings.my_list }} {% schema %} { "name": "section-settings-example", "tag": "section", "settings": [ { "id": "my_list", "label": "t:my-setting.label", "type": "product_list" } ] } {% endschema %} `; const ast = toLiquidHtmlAST(sourceCode); const variableOutput = ast.children[0]; assert(isLiquidVariableOutput(variableOutput)); const inferredType = await typeSystem.inferType( variableOutput.markup, ast, 'file:///sections/my-section.liquid', ); expect(inferredType).to.eql({ kind: 'array', valueType: 'product' } as ArrayType); }); it('should support block settings in blocks files', async () => { const sourceCode = ` {{ block.settings.my_list }} {% schema %} { "name": "section-settings-example", "tag": "section", "settings": [ { "id": "my_list", "label": "t:my-setting.label", "type": "product_list" } ] } {% endschema %} `; const ast = toLiquidHtmlAST(sourceCode); const variableOutput = ast.children[0]; assert(isLiquidVariableOutput(variableOutput)); const inferredType = await typeSystem.inferType( variableOutput.markup, ast, 'file:///blocks/my-section.liquid', ); expect(inferredType).to.eql({ kind: 'array', valueType: 'product' } as ArrayType); }); // TODO it.skip('should support narrowing the type of blocks', async () => { const sourceCode = ` {% for block in section.blocks %} {% case block.type %} {% when 'slide' %} {{ block.settings.image }} {% else %} {% endcase } {% if block.type == 'slide' %} {{ block.settings.image }} {% endif %} {% endfor %} {% schema %} { "name": "Slideshow", "tag": "section", "class": "slideshow", "settings": [], "blocks": [ { "name": "Slide", "type": "slide", "settings": [ { "type": "image_picker", "id": "image", "label": "Image" } ] } ] } {% endschema %} `; const ast = toLiquidHtmlAST(sourceCode); }); it('should support path-contextual variable types', async () => { let inferredType: string | ArrayType; const contexts: [string, string][] = [ ['section', 'sections/my-section.liquid'], ['comment', 'sections/main-article.liquid'], ['block', 'blocks/my-block.liquid'], ['predictive_search', 'sections/predictive-search.liquid'], ['recommendations', 'sections/recommendations.liquid'], ['app', 'blocks/recommendations.liquid'], ['app', 'snippets/recommendations.liquid'], ['locale', 'layout/checkout.liquid'], ]; for (const [object, path] of contexts) { const sourceCode = `{{ ${object} }}`; const ast = toLiquidHtmlAST(sourceCode); const variableOutput = ast.children[0]; assert(isLiquidVariableOutput(variableOutput)); inferredType = await typeSystem.inferType( variableOutput.markup, ast, // This will be different on Windows ^^ pathUtils.normalize(URI.from({ scheme: 'file', path })), ); expect(inferredType).to.eql(object); inferredType = await typeSystem.inferType( variableOutput.markup, ast, // This will be different on Windows ^^ pathUtils.normalize(URI.from({ scheme: 'file', path: 'file.liquid' })), ); expect(inferredType).to.eql('untyped'); } }); });