diff --git a/.changeset/contextual-objects-in-snippets.md b/.changeset/contextual-objects-in-snippets.md new file mode 100644 index 000000000..f607de9c2 --- /dev/null +++ b/.changeset/contextual-objects-in-snippets.md @@ -0,0 +1,5 @@ +--- +'@shopify/theme-check-common': patch +--- + +Recognise contextual Liquid objects in snippets based on the files that render them. diff --git a/packages/theme-check-common/src/checks/undefined-object/index.ts b/packages/theme-check-common/src/checks/undefined-object/index.ts index 4c04c3cc3..ef8cfa925 100644 --- a/packages/theme-check-common/src/checks/undefined-object/index.ts +++ b/packages/theme-check-common/src/checks/undefined-object/index.ts @@ -26,6 +26,7 @@ import { } from '../../types'; import { isError, last } from '../../utils'; import { hasLiquidDoc } from '../../liquid-doc/liquidDoc'; +import { getAncestorUris } from '../../utils/traversal'; import { isWithinRawTagThatDoesNotParseItsContents } from '../utils'; type Scope = { start?: number; end?: number }; @@ -156,7 +157,7 @@ export const UndefinedObject: LiquidCheckDefinition = { }, async onCodePathEnd() { - const objects = await globalObjects(themeDocset, relativePath, context.mode); + const objects = await globalObjects(themeDocset, context); objects.forEach((obj) => fileScopedVariables.add(obj.name)); (await themeBlockParameterNames(context)).forEach((name) => fileScopedVariables.add(name)); @@ -183,20 +184,29 @@ export const UndefinedObject: LiquidCheckDefinition = { }, }; -async function globalObjects(themeDocset: ThemeDocset, relativePath: string, mode: Mode = 'theme') { +async function globalObjects( + themeDocset: ThemeDocset, + context: Context, +) { const objects = await themeDocset.objects(); - const contextualObjects = getContextualObjects(relativePath, mode); + const relativePath = context.toRelativePath(context.file.uri); + const contextualObjects = new Set(getContextualObjects(relativePath, context.mode)); + + if (relativePath.startsWith('snippets/') && context.getReferences) { + const ancestors = await getAncestorUris(context.file.uri, context.getReferences); + ancestors.forEach((uri) => { + const ancestorPath = context.toRelativePath(uri); + getContextualObjects(ancestorPath, context.mode).forEach((name) => + contextualObjects.add(name), + ); + }); + } - const globalObjects = objects.filter(({ access, name }) => { + return objects.filter(({ access, name }) => { return ( - contextualObjects.includes(name) || - !access || - access.global === true || - access.template.length > 0 + contextualObjects.has(name) || !access || access.global === true || access.template.length > 0 ); }); - - return globalObjects; } /** Theme blocks always receive the built-in `content` parameter as a variable. */ diff --git a/specs/checks/undefined-object.yml b/specs/checks/undefined-object.yml index 4122443ec..8d33d6bb6 100644 --- a/specs/checks/undefined-object.yml +++ b/specs/checks/undefined-object.yml @@ -391,6 +391,24 @@ cases: message: "Unknown object 'recommendations' used." start: [0, 47] highlight: recommendations + - name: should allow section contextual objects in a snippet referenced by that section + mode: theme + files: + sections/predictive-search.liquid: "{% render 'predictive-search-results' %}" + snippets/predictive-search-results.liquid: "{% doc %} @param {string} x - x {% enddoc %}{{ predictive_search }}" + docset: default + references: + - source: sections/predictive-search.liquid + target: snippets/predictive-search-results.liquid + - name: should report section contextual objects in an unreferenced snippet + files: + snippets/my-snippet.liquid: "{% doc %} @param {string} x - x {% enddoc %}{{ predictive_search }}" + docset: default + offenses: + - file: snippets/my-snippet.liquid + message: "Unknown object 'predictive_search' used." + start: [0, 47] + highlight: predictive_search - name: should support contextual exceptions for checkout.liquid (1) files: layout/checkout.liquid: "{{ locale }}"