From 5c6a733aa3fafb5d7779594d15857b1e7dedbeae Mon Sep 17 00:00:00 2001 From: Rachael Sewell Date: Mon, 18 Dec 2023 14:35:12 -0800 Subject: [PATCH] add new rai linter rule (#47317) Co-authored-by: Peter Bengtsson --- src/content-linter/lib/linting-rules/index.js | 2 + .../lib/linting-rules/rai-reusable-usage.js | 58 ++++++++++ src/content-linter/style/github-docs.js | 5 + .../tests/unit/liquid-versioning.js | 2 +- .../tests/unit/rai-resuable-usage.js | 101 ++++++++++++++++++ .../data/reusables/nested_reusables/nested.md | 3 + .../rai/not_referencing_this_directory.md | 3 + .../fixtures/data/reusables/rai/note.md | 1 + .../reusables/rai/referencing_rai_data.md | 3 + .../reusables/rai/referencing_variable.md | 3 + 10 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 src/content-linter/lib/linting-rules/rai-reusable-usage.js create mode 100644 src/content-linter/tests/unit/rai-resuable-usage.js create mode 100644 src/fixtures/fixtures/data/reusables/nested_reusables/nested.md create mode 100644 src/fixtures/fixtures/data/reusables/rai/not_referencing_this_directory.md create mode 100644 src/fixtures/fixtures/data/reusables/rai/note.md create mode 100644 src/fixtures/fixtures/data/reusables/rai/referencing_rai_data.md create mode 100644 src/fixtures/fixtures/data/reusables/rai/referencing_variable.md diff --git a/src/content-linter/lib/linting-rules/index.js b/src/content-linter/lib/linting-rules/index.js index 46a8345d5032..db35293cdb17 100644 --- a/src/content-linter/lib/linting-rules/index.js +++ b/src/content-linter/lib/linting-rules/index.js @@ -26,6 +26,7 @@ import { frontmatterSchema } from './frontmatter-schema.js' import { codeAnnotations } from './code-annotations.js' import { frontmatterLiquidSyntax, liquidSyntax } from './liquid-syntax.js' import { liquidIfTags, liquidIfVersionTags } from './liquid-versioning.js' +import { raiReusableUsage } from './rai-reusable-usage.js' const noDefaultAltText = markdownlintGitHub.find((elem) => elem.names.includes('no-default-alt-text'), @@ -65,5 +66,6 @@ export const gitHubDocsMarkdownlint = { liquidSyntax, liquidIfTags, liquidIfVersionTags, + raiReusableUsage, ], } diff --git a/src/content-linter/lib/linting-rules/rai-reusable-usage.js b/src/content-linter/lib/linting-rules/rai-reusable-usage.js new file mode 100644 index 000000000000..dcaf30dfab01 --- /dev/null +++ b/src/content-linter/lib/linting-rules/rai-reusable-usage.js @@ -0,0 +1,58 @@ +import { addError } from 'markdownlint-rule-helpers' +import { TokenKind } from 'liquidjs' +import path from 'path' + +import { getFrontmatter } from '../helpers/utils.js' +import { getLiquidTokens, getPositionData } from '../helpers/liquid-utils.js' + +export const raiReusableUsage = { + names: ['GHD035', 'rai-reusable-usage'], + description: + 'RAI articles and reusables can only reference reusable content in the data/reusables/rai directory', + tags: ['feature', 'rai'], + function: (params, onError) => { + if (!isFileRai(params)) return + + const content = params.lines.join('\n') + const tokens = getLiquidTokens(content) + .filter((token) => token.kind === TokenKind.Tag) + .filter((token) => token.name === 'data' || token.name === 'indented_data_reference') + // It's ok to reference variables from rai content + .filter((token) => !token.args.startsWith('variables')) + + for (const token of tokens) { + // if token is 'data foo.bar` or `indented_data_reference foo.bar depth=3` + // we only want the `foo.bar` part. + const dataDirectoryReference = token.args.split(/\s+/)[0] + if (dataDirectoryReference.startsWith('reusables.rai')) continue + + const lines = params.lines + const { lineNumber, column, length } = getPositionData(token, lines) + addError( + onError, + lineNumber, + `RAI reusables and content articles can only reference reusables in the data/reusables/rai directory to ensure that changes to RAI content are reviewed by the legal-product team. The Liquid data reference {% ${token.content} %} needs to be moved to the data/reusables/rai directory.`, + token.content, + [column, length], + null, // No fix available + ) + } + }, +} + +// Rai file content can be in either the data/reusables/rai directory +// or anywhere in the content directory +function isFileRai(params) { + // ROOT is set in the test environment to src/fixtures/fixtures otherwise + // it is set to the root of the project. + const ROOT = process.env.ROOT || '.' + const dataPath = path.join(ROOT, 'data/reusables') + const dataRai = path.join(dataPath, 'rai') + + if (params.name.startsWith(dataPath)) { + return params.name.startsWith(dataRai) + } + + const fm = getFrontmatter(params.frontMatterLines) || {} + return fm.type === 'rai' +} diff --git a/src/content-linter/style/github-docs.js b/src/content-linter/style/github-docs.js index 60691142284e..626e9cce3d66 100644 --- a/src/content-linter/style/github-docs.js +++ b/src/content-linter/style/github-docs.js @@ -104,6 +104,11 @@ const githubDocsConfig = { severity: 'warning', 'partial-markdown-files': true, }, + 'rai-reusable-usage': { + // GHD035 + severity: 'error', + 'partial-markdown-files': true, + }, } export const githubDocsFrontmatterConfig = { diff --git a/src/content-linter/tests/unit/liquid-versioning.js b/src/content-linter/tests/unit/liquid-versioning.js index aa1abf484d21..bc52951053d3 100644 --- a/src/content-linter/tests/unit/liquid-versioning.js +++ b/src/content-linter/tests/unit/liquid-versioning.js @@ -39,7 +39,7 @@ describe(liquidIfVersionTags.names.join(' - '), () => { const envVarValueBefore = process.env.ROOT beforeAll(() => { - process.env.ROOT = path.join('src', 'fixtures', 'fixtures') + process.env.ROOT = 'src/fixtures/fixtures' }) afterAll(() => { diff --git a/src/content-linter/tests/unit/rai-resuable-usage.js b/src/content-linter/tests/unit/rai-resuable-usage.js new file mode 100644 index 000000000000..dfd7f132b0f6 --- /dev/null +++ b/src/content-linter/tests/unit/rai-resuable-usage.js @@ -0,0 +1,101 @@ +import { runRule } from '../../lib/init-test.js' +import { raiReusableUsage } from '../../lib/linting-rules/rai-reusable-usage.js' + +describe(raiReusableUsage.names.join(' - '), () => { + const envVarValueBefore = process.env.ROOT + + beforeAll(() => { + process.env.ROOT = 'src/fixtures/fixtures' + }) + + afterAll(() => { + process.env.ROOT = envVarValueBefore + }) + + test('a non-RAI content article referencing non-RAI data succeeds', async () => { + const markdown = [ + '---', + 'title: article', + '---', + '', + '{% data reusables.injectables.multiple_numbers %}', + ].join('\n') + const result = await runRule(raiReusableUsage, { + strings: { markdown }, + }) + const errors = result.markdown + expect(errors.length).toBe(0) + }) + + test('an RAI content article referencing non-RAI data fails', async () => { + const markdown = [ + '---', + 'title: article', + 'type: rai', + '---', + '', + '{% data reusables.injectables.multiple_numbers %}', + ].join('\n') + const result = await runRule(raiReusableUsage, { + strings: { markdown }, + }) + const errors = result.markdown + expect(errors.length).toBe(1) + expect(errors[0].lineNumber).toBe(6) + expect(errors[0].errorRange).toEqual([1, 49]) + }) + + test('an RAI content article referencing RAI data succeeds', async () => { + const markdown = [ + '---', + 'title: article', + 'type: rai', + '---', + '', + '{% data reusables.rai.note %}', + ].join('\n') + const result = await runRule(raiReusableUsage, { + strings: { markdown }, + }) + const errors = result.markdown + expect(errors.length).toBe(0) + }) + + test('a non-RAI data file referencing non-RAI data succeeds', async () => { + const TEST_FILE = 'src/fixtures/fixtures/data/reusables/nested_reusables/nested.md' + const result = await runRule(raiReusableUsage, { + files: [TEST_FILE], + }) + const errors = result[TEST_FILE] + expect(errors.length).toBe(0) + }) + + test('an RAI data file referencing RAI data succeeds', async () => { + const TEST_FILE = 'src/fixtures/fixtures/data/reusables/rai/referencing_rai_data.md' + const result = await runRule(raiReusableUsage, { + files: [TEST_FILE], + }) + const errors = result[TEST_FILE] + expect(errors.length).toBe(0) + }) + + test('an RAI data file referencing non-RAI data fails', async () => { + const TEST_FILE = 'src/fixtures/fixtures/data/reusables/rai/not_referencing_this_directory.md' + const result = await runRule(raiReusableUsage, { + files: [TEST_FILE], + }) + const errors = result[TEST_FILE] + expect(errors.length).toBe(1) + expect(errors[0].lineNumber).toBe(3) + expect(errors[0].errorRange).toEqual([1, 41]) + }) + + test('an RAI data file referencing data variables succeeds', async () => { + const TEST_FILE = 'src/fixtures/fixtures/data/reusables/rai/referencing_variable.md' + const result = await runRule(raiReusableUsage, { + files: [TEST_FILE], + }) + const errors = result[TEST_FILE] + expect(errors.length).toBe(0) + }) +}) diff --git a/src/fixtures/fixtures/data/reusables/nested_reusables/nested.md b/src/fixtures/fixtures/data/reusables/nested_reusables/nested.md new file mode 100644 index 000000000000..d54287b67dc8 --- /dev/null +++ b/src/fixtures/fixtures/data/reusables/nested_reusables/nested.md @@ -0,0 +1,3 @@ +This file contains reusable text and also references reusable text. + +{% data reusables.policies.translation %} \ No newline at end of file diff --git a/src/fixtures/fixtures/data/reusables/rai/not_referencing_this_directory.md b/src/fixtures/fixtures/data/reusables/rai/not_referencing_this_directory.md new file mode 100644 index 000000000000..033db38724fe --- /dev/null +++ b/src/fixtures/fixtures/data/reusables/rai/not_referencing_this_directory.md @@ -0,0 +1,3 @@ +This is responsible AI content that is incorrectly referencing data in the non-rai directory. + +{% data reusables.policies.translation %} \ No newline at end of file diff --git a/src/fixtures/fixtures/data/reusables/rai/note.md b/src/fixtures/fixtures/data/reusables/rai/note.md new file mode 100644 index 000000000000..1ecdc21d0569 --- /dev/null +++ b/src/fixtures/fixtures/data/reusables/rai/note.md @@ -0,0 +1 @@ +This is responsible AI content. \ No newline at end of file diff --git a/src/fixtures/fixtures/data/reusables/rai/referencing_rai_data.md b/src/fixtures/fixtures/data/reusables/rai/referencing_rai_data.md new file mode 100644 index 000000000000..1e9867a90aa8 --- /dev/null +++ b/src/fixtures/fixtures/data/reusables/rai/referencing_rai_data.md @@ -0,0 +1,3 @@ +This is an RAI data reusable referencing other RAI data reusables. + +{% data reusables.rai.note %} \ No newline at end of file diff --git a/src/fixtures/fixtures/data/reusables/rai/referencing_variable.md b/src/fixtures/fixtures/data/reusables/rai/referencing_variable.md new file mode 100644 index 000000000000..d1ce41338b2a --- /dev/null +++ b/src/fixtures/fixtures/data/reusables/rai/referencing_variable.md @@ -0,0 +1,3 @@ +Referencing data variables is ok. + +{% data variables.product.name %}