diff --git a/docs/cli/json-output.md b/docs/cli/json-output.md index 9f2aaf4fd3e..edd16894491 100644 --- a/docs/cli/json-output.md +++ b/docs/cli/json-output.md @@ -32,7 +32,7 @@ Progress and diagnostics use JSON side events on stderr. | Shared meanings | Use `name`, `clientId`, `previousVersion`, and distinct `planHandle`/`planName`. Resource URL names and placement agree across commands. Different projections may expose different fields, but a shared field keeps its meaning. | | Numbers and enums | Bulk `objectCount` is a nonnegative decimal string; reject unsafe numeric inputs. Bounded counts and positions are nonnegative integers; money uses strings. CLI-owned enums are closed, with kebab-case multiword values. Upstream enums remain extensible strings with documented known values. | | Files | Use absolute native filesystem paths: `path` for the primary artifact, `directory` for a project directory, descriptive `...Path` fields for additional files. Relative route names stay route names. | -| Diagnostics | Use `filePath` for filesystem paths and array-valued `fieldPath` for configuration fields. JSON line/column positions are zero-based; text presenters can convert them. | +| Diagnostics | Use `filePath` for filesystem paths and array-valued `fieldPath` for configuration fields. JSON line/column positions are one-based, matching human-readable output. | | Pagination | Use `pageInfo` when results are capped, retain totals already fetched, and distinguish unknown values from `false` or zero. Do not invent a cursor. Document whether the list is complete or one page. | | Versioning | Version CLI result contracts with the CLI release. Independently versioned artifact formats keep their own versions; do not introduce a command-specific result version field. | diff --git a/packages/cli/README.md b/packages/cli/README.md index f990691bd9c..5968afe5492 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -14151,23 +14151,23 @@ DESCRIPTION }, "startRow": { "type": "integer", - "minimum": 0, - "description": "The zero-based starting line." + "exclusiveMinimum": 0, + "description": "The one-based starting line." }, "startColumn": { "type": "integer", - "minimum": 0, - "description": "The zero-based starting column." + "exclusiveMinimum": 0, + "description": "The one-based starting column." }, "endRow": { "type": "integer", - "minimum": 0, - "description": "The zero-based ending line." + "exclusiveMinimum": 0, + "description": "The one-based ending line." }, "endColumn": { "type": "integer", - "minimum": 0, - "description": "The zero-based ending column." + "exclusiveMinimum": 0, + "description": "The one-based ending column." }, "message": { "type": "string" diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 8bcf1aaa2d0..28552db7573 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -10534,7 +10534,7 @@ "args": { }, "customPluginName": "@shopify/theme", - "description": "Calls and runs \"Theme Check\" (https://shopify.dev/docs/themes/tools/theme-check) to analyze your theme code for errors and to ensure that it follows theme and Liquid best practices. \"Learn more about the checks that Theme Check runs.\" (https://shopify.dev/docs/themes/tools/theme-check/checks)\n\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemeCheckResult` schema.\n\n```json\n{\n \"anyOf\": [\n {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/0/properties/result\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environments\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeEnvironment\"\n }\n }\n },\n \"required\": [\n \"environments\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"title\": \"ThemeCheckResult\",\n \"definitions\": {\n \"ThemeCheckIssue\": {\n \"type\": \"object\",\n \"properties\": {\n \"filePath\": {\n \"type\": \"string\",\n \"description\": \"The absolute native path of the file.\"\n },\n \"check\": {\n \"type\": \"string\"\n },\n \"severity\": {\n \"type\": \"string\",\n \"enum\": [\n \"error\",\n \"warning\",\n \"info\"\n ]\n },\n \"startRow\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"description\": \"The zero-based starting line.\"\n },\n \"startColumn\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"description\": \"The zero-based starting column.\"\n },\n \"endRow\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"description\": \"The zero-based ending line.\"\n },\n \"endColumn\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"description\": \"The zero-based ending column.\"\n },\n \"message\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"filePath\",\n \"check\",\n \"severity\",\n \"startRow\",\n \"startColumn\",\n \"endRow\",\n \"endColumn\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n \"ThemeEnvironment\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"result\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"valid\": {\n \"type\": \"boolean\",\n \"description\": \"Whether the validation passes the selected blocking policy.\"\n },\n \"issues\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeCheckIssue\"\n }\n },\n \"errorCount\": {\n \"type\": \"integer\",\n \"minimum\": 0\n },\n \"warningCount\": {\n \"type\": \"integer\",\n \"minimum\": 0\n },\n \"infoCount\": {\n \"type\": \"integer\",\n \"minimum\": 0\n }\n },\n \"required\": [\n \"valid\",\n \"issues\",\n \"errorCount\",\n \"warningCount\",\n \"infoCount\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"cancelled\"\n }\n },\n \"required\": [\n \"status\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"result\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"abort\"\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"A stable error code, included only when known.\"\n },\n \"tryMessage\": {\n \"type\": \"string\"\n },\n \"nextSteps\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"customSections\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"body\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n ]\n }\n },\n \"required\": [\n \"body\"\n ],\n \"additionalProperties\": false\n }\n },\n \"details\": {\n \"description\": \"Selected domain details, preserving native API payloads such as GraphQL errors, extensions, and data.\"\n }\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"bug\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"code\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/code\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"stack\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"external\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"code\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/code\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"args\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"message\",\n \"command\",\n \"args\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"error\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", + "description": "Calls and runs \"Theme Check\" (https://shopify.dev/docs/themes/tools/theme-check) to analyze your theme code for errors and to ensure that it follows theme and Liquid best practices. \"Learn more about the checks that Theme Check runs.\" (https://shopify.dev/docs/themes/tools/theme-check/checks)\n\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemeCheckResult` schema.\n\n```json\n{\n \"anyOf\": [\n {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/0/properties/result\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environments\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeEnvironment\"\n }\n }\n },\n \"required\": [\n \"environments\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"title\": \"ThemeCheckResult\",\n \"definitions\": {\n \"ThemeCheckIssue\": {\n \"type\": \"object\",\n \"properties\": {\n \"filePath\": {\n \"type\": \"string\",\n \"description\": \"The absolute native path of the file.\"\n },\n \"check\": {\n \"type\": \"string\"\n },\n \"severity\": {\n \"type\": \"string\",\n \"enum\": [\n \"error\",\n \"warning\",\n \"info\"\n ]\n },\n \"startRow\": {\n \"type\": \"integer\",\n \"exclusiveMinimum\": 0,\n \"description\": \"The one-based starting line.\"\n },\n \"startColumn\": {\n \"type\": \"integer\",\n \"exclusiveMinimum\": 0,\n \"description\": \"The one-based starting column.\"\n },\n \"endRow\": {\n \"type\": \"integer\",\n \"exclusiveMinimum\": 0,\n \"description\": \"The one-based ending line.\"\n },\n \"endColumn\": {\n \"type\": \"integer\",\n \"exclusiveMinimum\": 0,\n \"description\": \"The one-based ending column.\"\n },\n \"message\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"filePath\",\n \"check\",\n \"severity\",\n \"startRow\",\n \"startColumn\",\n \"endRow\",\n \"endColumn\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n \"ThemeEnvironment\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"result\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"valid\": {\n \"type\": \"boolean\",\n \"description\": \"Whether the validation passes the selected blocking policy.\"\n },\n \"issues\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeCheckIssue\"\n }\n },\n \"errorCount\": {\n \"type\": \"integer\",\n \"minimum\": 0\n },\n \"warningCount\": {\n \"type\": \"integer\",\n \"minimum\": 0\n },\n \"infoCount\": {\n \"type\": \"integer\",\n \"minimum\": 0\n }\n },\n \"required\": [\n \"valid\",\n \"issues\",\n \"errorCount\",\n \"warningCount\",\n \"infoCount\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"cancelled\"\n }\n },\n \"required\": [\n \"status\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"result\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"abort\"\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"A stable error code, included only when known.\"\n },\n \"tryMessage\": {\n \"type\": \"string\"\n },\n \"nextSteps\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"customSections\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"body\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n ]\n }\n },\n \"required\": [\n \"body\"\n ],\n \"additionalProperties\": false\n }\n },\n \"details\": {\n \"description\": \"Selected domain details, preserving native API payloads such as GraphQL errors, extensions, and data.\"\n }\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"bug\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"code\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/code\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"stack\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"external\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"code\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/code\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"args\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"message\",\n \"command\",\n \"args\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"error\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", "descriptionWithMarkdown": "Calls and runs [Theme Check](https://shopify.dev/docs/themes/tools/theme-check) to analyze your theme code for errors and to ensure that it follows theme and Liquid best practices. [Learn more about the checks that Theme Check runs.](https://shopify.dev/docs/themes/tools/theme-check/checks)", "enableJsonFlag": false, "flags": { diff --git a/packages/theme/src/cli/commands/theme/check-json.test.ts b/packages/theme/src/cli/commands/theme/check-json.test.ts index 942ee085ff7..21fcb27344d 100644 --- a/packages/theme/src/cli/commands/theme/check-json.test.ts +++ b/packages/theme/src/cli/commands/theme/check-json.test.ts @@ -34,10 +34,10 @@ const invalidResult = { filePath: resolvePath(pathUtils.fsPath(offense.uri)), check: 'ExampleCheck', severity: 'error', - startRow: 0, - startColumn: 0, - endRow: 0, - endColumn: 1, + startRow: 1, + startColumn: 1, + endRow: 1, + endColumn: 2, message: 'Example offense', }, ], @@ -53,7 +53,7 @@ async function configuration() { return config } -test('projects the service model to flat issues with strict zero-based positions and counts', async () => { +test('projects the service model to flat issues with strict one-based positions and counts', async () => { expect(Check.jsonOutputSchema).toBe(themeCheckJsonOutputSchema) vi.mocked(themeCheckRun).mockResolvedValue({offenses: [offense], theme: [], config: {} as never}) const {result} = await checkTheme('/theme') @@ -62,9 +62,6 @@ test('projects the service model to flat issues with strict zero-based positions expect(JSON.parse(encodeThemeCheckResult([]))).toEqual(emptyResult) expect(() => themeCheckJsonOutputSchema.validate({...invalidResult, errorCount: '1'})).toThrow() expect(() => themeCheckJsonOutputSchema.validate({...invalidResult, extra: true})).toThrow() - expect(() => - themeCheckJsonOutputSchema.validate({...invalidResult, issues: [{...invalidResult.issues[0], startRow: -1}]}), - ).toThrow() expect(() => themeCheckJsonOutputSchema.validate({ ...invalidResult, @@ -73,6 +70,31 @@ test('projects the service model to flat issues with strict zero-based positions ).toThrow() }) +test('converts multiline ranges to one-based positions', () => { + const multilineOffense = { + ...offense, + start: {index: 14, line: 2, character: 4}, + end: {index: 35, line: 5, character: 0}, + } + const result = formatOffensesJson(sortOffenses([multilineOffense])) + + expect(JSON.parse(encodeThemeCheckResult(result))).toEqual({ + ...invalidResult, + issues: [{...invalidResult.issues[0], startRow: 3, startColumn: 5, endRow: 6, endColumn: 1}], + }) +}) + +test.each(['startRow', 'startColumn', 'endRow', 'endColumn'])('requires a positive integer for %s', (position) => { + for (const value of [0, -1, 1.5]) { + expect(() => + themeCheckJsonOutputSchema.validate({ + ...invalidResult, + issues: [{...invalidResult.issues[0], [position]: value}], + }), + ).toThrow() + } +}) + test.each(['--json', '--output=json'])('writes one validation result and exits nonzero for %s', async (flag) => { await inTemporaryDirectory(async (directory) => { const config = await configuration() diff --git a/packages/theme/src/cli/services/check/types.ts b/packages/theme/src/cli/services/check/types.ts index 7df2c5aec5e..1e39c73dba9 100644 --- a/packages/theme/src/cli/services/check/types.ts +++ b/packages/theme/src/cli/services/check/types.ts @@ -10,10 +10,10 @@ const ThemeCheckIssueSchema = zod filePath: zod.string().refine(isAbsolutePath).describe('The absolute native path of the file.'), check: zod.string(), severity: zod.enum(['error', 'warning', 'info']), - startRow: zod.number().int().nonnegative().describe('The zero-based starting line.'), - startColumn: zod.number().int().nonnegative().describe('The zero-based starting column.'), - endRow: zod.number().int().nonnegative().describe('The zero-based ending line.'), - endColumn: zod.number().int().nonnegative().describe('The zero-based ending column.'), + startRow: zod.number().int().positive().describe('The one-based starting line.'), + startColumn: zod.number().int().positive().describe('The one-based starting column.'), + endRow: zod.number().int().positive().describe('The one-based ending line.'), + endColumn: zod.number().int().positive().describe('The one-based ending column.'), message: zod.string(), }) .strict() @@ -61,10 +61,10 @@ export const themeCheckJsonOutputSchema = defineThemeJsonOutputSchema({ filePath: resolvePath(file.path), check: offense.check, severity: offense.severity, - startRow: offense.start_row, - startColumn: offense.start_column, - endRow: offense.end_row, - endColumn: offense.end_column, + startRow: offense.start_row + 1, + startColumn: offense.start_column + 1, + endRow: offense.end_row + 1, + endColumn: offense.end_column + 1, message: offense.message, })), ),