Fix TODOCS placeholder linting exclusion for documentation file (#57551)
This commit is contained in:
@@ -8,7 +8,7 @@ versions:
|
||||
ghes: '*'
|
||||
---
|
||||
|
||||
<!-- markdownlint-disable search-replace -->
|
||||
|
||||
## Using the TODOCS placeholder
|
||||
|
||||
Sometimes technical writers use placeholders while writing documentation to remind themselves to come back to something later. It's a useful technique, but there's always the possibility that the placeholder will be overlooked and slip into production. At that point, the only way the Docs team will find out about it is if someone sees it and reports it.
|
||||
@@ -27,4 +27,3 @@ To prevent slips, use the string `TODOCS` as your placeholder. The Docs test sui
|
||||
|
||||
1. Click **Sign in & Turn on**, then select the account to which you want your settings to be synced.
|
||||
```
|
||||
<!-- markdownlint-enable search-replace -->
|
||||
|
||||
67
src/content-linter/lib/helpers/should-include-result.ts
Normal file
67
src/content-linter/lib/helpers/should-include-result.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import * as nodePath from 'path'
|
||||
import { reportingConfig } from '@/content-linter/style/github-docs'
|
||||
|
||||
interface LintFlaw {
|
||||
severity: string
|
||||
ruleNames: string[]
|
||||
errorDetail?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a lint result should be included based on reporting configuration
|
||||
*
|
||||
* @param flaw - The lint flaw object containing rule names, severity, etc.
|
||||
* @param filePath - The path of the file being linted
|
||||
* @returns true if the flaw should be included, false if it should be excluded
|
||||
*/
|
||||
export function shouldIncludeResult(flaw: LintFlaw, filePath: string): boolean {
|
||||
if (!flaw.ruleNames || !Array.isArray(flaw.ruleNames)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Extract all possible rule names including sub-rules from search-replace
|
||||
const allRuleNames = [...flaw.ruleNames]
|
||||
|
||||
// For search-replace rules, extract the sub-rule name from errorDetail
|
||||
if (flaw.ruleNames.includes('search-replace') && flaw.errorDetail) {
|
||||
const match = flaw.errorDetail.match(/^([^:]+):/)
|
||||
if (match) {
|
||||
allRuleNames.push(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any rule name is in the exclude list
|
||||
const hasExcludedRule = allRuleNames.some((ruleName: string) =>
|
||||
reportingConfig.excludeRules.includes(ruleName),
|
||||
)
|
||||
if (hasExcludedRule) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if this specific file should be excluded for any of the rules
|
||||
for (const ruleName of allRuleNames) {
|
||||
const excludedFiles =
|
||||
reportingConfig.excludeFilesFromRules?.[
|
||||
ruleName as keyof typeof reportingConfig.excludeFilesFromRules
|
||||
]
|
||||
if (
|
||||
excludedFiles &&
|
||||
excludedFiles.some((excludedPath: string) => {
|
||||
// Normalize paths for comparison
|
||||
const normalizedFilePath = nodePath.normalize(filePath)
|
||||
const normalizedExcludedPath = nodePath.normalize(excludedPath)
|
||||
return (
|
||||
normalizedFilePath === normalizedExcludedPath ||
|
||||
normalizedFilePath.endsWith(normalizedExcludedPath)
|
||||
)
|
||||
})
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Default to true - include everything unless explicitly excluded
|
||||
// This function only handles exclusions; reporting-specific inclusion logic
|
||||
// (like severity/rule filtering) is handled separately in lint-report.ts
|
||||
return true
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { prettyPrintResults } from './pretty-print-results'
|
||||
import { getLintableYml } from '@/content-linter/lib/helpers/get-lintable-yml'
|
||||
import { printAnnotationResults } from '../lib/helpers/print-annotations'
|
||||
import languages from '@/languages/lib/languages'
|
||||
import { shouldIncludeResult } from '../lib/helpers/should-include-result'
|
||||
|
||||
program
|
||||
.description('Run GitHub Docs Markdownlint rules.')
|
||||
@@ -426,7 +427,9 @@ function getFormattedResults(allResults, isPrecommit) {
|
||||
if (verbose) {
|
||||
output[key] = [...results]
|
||||
} else {
|
||||
const formattedResults = results.map((flaw) => formatResult(flaw, isPrecommit))
|
||||
const formattedResults = results
|
||||
.map((flaw) => formatResult(flaw, isPrecommit))
|
||||
.filter((flaw) => shouldIncludeResult(flaw, key))
|
||||
const errors = formattedResults.filter((result) => result.severity === 'error')
|
||||
const warnings = formattedResults.filter((result) => result.severity === 'warning')
|
||||
const sortedResult = [...errors, ...warnings]
|
||||
|
||||
@@ -5,6 +5,7 @@ import coreLib from '@actions/core'
|
||||
import github from '@/workflows/github'
|
||||
import { getEnvInputs } from '@/workflows/get-env-inputs'
|
||||
import { createReportIssue, linkReports } from '@/workflows/issue-report'
|
||||
import { shouldIncludeResult } from '@/content-linter/lib/helpers/should-include-result'
|
||||
import { reportingConfig } from '@/content-linter/style/github-docs'
|
||||
|
||||
// GitHub issue body size limit is ~65k characters, so we'll use 60k as a safe limit
|
||||
@@ -13,31 +14,40 @@ const MAX_ISSUE_BODY_SIZE = 60000
|
||||
interface LintFlaw {
|
||||
severity: string
|
||||
ruleNames: string[]
|
||||
errorDetail?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a lint result should be included in the automated report
|
||||
* Uses shared exclusion logic with additional reporting-specific filtering
|
||||
*/
|
||||
function shouldIncludeInReport(flaw: LintFlaw): boolean {
|
||||
function shouldIncludeInReport(flaw: LintFlaw, filePath: string): boolean {
|
||||
if (!flaw.ruleNames || !Array.isArray(flaw.ruleNames)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if any rule name is in the exclude list
|
||||
const hasExcludedRule = flaw.ruleNames.some((ruleName: string) =>
|
||||
reportingConfig.excludeRules.includes(ruleName),
|
||||
)
|
||||
if (hasExcludedRule) {
|
||||
// First check if it should be excluded (file-specific or rule-specific exclusions)
|
||||
if (!shouldIncludeResult(flaw, filePath)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Extract all possible rule names including sub-rules from search-replace
|
||||
const allRuleNames = [...flaw.ruleNames]
|
||||
if (flaw.ruleNames.includes('search-replace') && flaw.errorDetail) {
|
||||
const match = flaw.errorDetail.match(/^([^:]+):/)
|
||||
if (match) {
|
||||
allRuleNames.push(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Apply reporting-specific filtering
|
||||
// Check if severity should be included
|
||||
if (reportingConfig.includeSeverities.includes(flaw.severity)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if any rule name is in the include list
|
||||
const hasIncludedRule = flaw.ruleNames.some((ruleName: string) =>
|
||||
const hasIncludedRule = allRuleNames.some((ruleName: string) =>
|
||||
reportingConfig.includeRules.includes(ruleName),
|
||||
)
|
||||
if (hasIncludedRule) {
|
||||
@@ -91,7 +101,7 @@ async function main() {
|
||||
// Filter results based on reporting configuration
|
||||
const filteredResults: Record<string, LintFlaw[]> = {}
|
||||
for (const [file, flaws] of Object.entries(parsedResults)) {
|
||||
const filteredFlaws = (flaws as LintFlaw[]).filter(shouldIncludeInReport)
|
||||
const filteredFlaws = (flaws as LintFlaw[]).filter((flaw) => shouldIncludeInReport(flaw, file))
|
||||
|
||||
// Only include files that have remaining flaws after filtering
|
||||
if (filteredFlaws.length > 0) {
|
||||
|
||||
@@ -16,6 +16,14 @@ export const reportingConfig = {
|
||||
// Example: 'GHD030' // Uncomment to exclude code-fence-line-length warnings
|
||||
'british-english-quotes', // Exclude from reports but keep for pre-commit
|
||||
],
|
||||
|
||||
// Files to exclude from specific rules in reports
|
||||
// Format: { 'rule-name': ['file/path/pattern1', 'file/path/pattern2'] }
|
||||
excludeFilesFromRules: {
|
||||
'todocs-placeholder': [
|
||||
'content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const githubDocsConfig = {
|
||||
|
||||
160
src/content-linter/tests/unit/lint-report-exclusions.js
Normal file
160
src/content-linter/tests/unit/lint-report-exclusions.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { shouldIncludeResult } from '../../lib/helpers/should-include-result'
|
||||
import { reportingConfig } from '../../style/github-docs'
|
||||
|
||||
describe('lint report exclusions', () => {
|
||||
// Helper function to simulate the reporting logic from lint-report.ts
|
||||
function shouldIncludeInReport(flaw, filePath) {
|
||||
if (!flaw.ruleNames || !Array.isArray(flaw.ruleNames)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// First check exclusions using shared function
|
||||
if (!shouldIncludeResult(flaw, filePath)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Extract all possible rule names including sub-rules from search-replace
|
||||
const allRuleNames = [...flaw.ruleNames]
|
||||
if (flaw.ruleNames.includes('search-replace') && flaw.errorDetail) {
|
||||
const match = flaw.errorDetail.match(/^([^:]+):/)
|
||||
if (match) {
|
||||
allRuleNames.push(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Apply reporting-specific filtering
|
||||
// Check if severity should be included
|
||||
if (reportingConfig.includeSeverities.includes(flaw.severity)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if any rule name is in the include list
|
||||
const hasIncludedRule = allRuleNames.some((ruleName) =>
|
||||
reportingConfig.includeRules.includes(ruleName),
|
||||
)
|
||||
if (hasIncludedRule) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
test('TODOCS placeholder errors are excluded for documentation file', () => {
|
||||
const flaw = {
|
||||
severity: 'error',
|
||||
ruleNames: ['search-replace'],
|
||||
errorDetail: 'todocs-placeholder: Catch occurrences of TODOCS placeholder.',
|
||||
}
|
||||
|
||||
const excludedFilePath =
|
||||
'content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md'
|
||||
const regularFilePath = 'content/some-other-article.md'
|
||||
|
||||
// Should be excluded for the specific documentation file
|
||||
expect(shouldIncludeInReport(flaw, excludedFilePath)).toBe(false)
|
||||
|
||||
// Should still be included for other files
|
||||
expect(shouldIncludeInReport(flaw, regularFilePath)).toBe(true)
|
||||
})
|
||||
|
||||
test('TODOCS placeholder errors are excluded with different path formats', () => {
|
||||
const flaw = {
|
||||
severity: 'error',
|
||||
ruleNames: ['search-replace'],
|
||||
errorDetail: 'todocs-placeholder: Catch occurrences of TODOCS placeholder.',
|
||||
}
|
||||
|
||||
// Test various path formats that should match
|
||||
const pathVariants = [
|
||||
'content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md',
|
||||
'./content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md',
|
||||
'/absolute/path/content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md',
|
||||
]
|
||||
|
||||
pathVariants.forEach((path) => {
|
||||
expect(shouldIncludeInReport(flaw, path)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
test('other rules are not affected by TODOCS file exclusions', () => {
|
||||
const flaw = {
|
||||
severity: 'error',
|
||||
ruleNames: ['docs-domain'],
|
||||
}
|
||||
|
||||
const excludedFilePath =
|
||||
'content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md'
|
||||
|
||||
// Should still be included for other rules even in the excluded file
|
||||
expect(shouldIncludeInReport(flaw, excludedFilePath)).toBe(true)
|
||||
})
|
||||
|
||||
test('multiple rule names with mixed exclusions', () => {
|
||||
const flaw = {
|
||||
severity: 'error',
|
||||
ruleNames: ['search-replace', 'docs-domain'],
|
||||
errorDetail: 'todocs-placeholder: Catch occurrences of TODOCS placeholder.',
|
||||
}
|
||||
|
||||
const excludedFilePath =
|
||||
'content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md'
|
||||
|
||||
// Should be excluded because one of the rules (todocs-placeholder) is excluded for this file
|
||||
expect(shouldIncludeInReport(flaw, excludedFilePath)).toBe(false)
|
||||
})
|
||||
|
||||
test('exclusion configuration exists and is properly structured', () => {
|
||||
expect(reportingConfig.excludeFilesFromRules).toBeDefined()
|
||||
expect(reportingConfig.excludeFilesFromRules['todocs-placeholder']).toBeDefined()
|
||||
expect(Array.isArray(reportingConfig.excludeFilesFromRules['todocs-placeholder'])).toBe(true)
|
||||
expect(
|
||||
reportingConfig.excludeFilesFromRules['todocs-placeholder'].includes(
|
||||
'content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md',
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
describe('shared shouldIncludeResult function', () => {
|
||||
test('excludes TODOCS placeholder errors for specific file', () => {
|
||||
const flaw = {
|
||||
severity: 'error',
|
||||
ruleNames: ['search-replace'],
|
||||
errorDetail: 'todocs-placeholder: Catch occurrences of TODOCS placeholder.',
|
||||
}
|
||||
|
||||
const excludedFilePath =
|
||||
'content/contributing/collaborating-on-github-docs/using-the-todocs-placeholder-to-leave-notes.md'
|
||||
const regularFilePath = 'content/some-other-article.md'
|
||||
|
||||
// Should be excluded for the specific documentation file
|
||||
expect(shouldIncludeResult(flaw, excludedFilePath)).toBe(false)
|
||||
|
||||
// Should be included for other files
|
||||
expect(shouldIncludeResult(flaw, regularFilePath)).toBe(true)
|
||||
})
|
||||
|
||||
test('includes flaws by default when no exclusions apply', () => {
|
||||
const flaw = {
|
||||
severity: 'error',
|
||||
ruleNames: ['some-other-rule'],
|
||||
}
|
||||
|
||||
const filePath = 'content/some-article.md'
|
||||
|
||||
expect(shouldIncludeResult(flaw, filePath)).toBe(true)
|
||||
})
|
||||
|
||||
test('handles missing errorDetail gracefully', () => {
|
||||
const flaw = {
|
||||
severity: 'error',
|
||||
ruleNames: ['search-replace'],
|
||||
// no errorDetail
|
||||
}
|
||||
|
||||
const filePath = 'content/some-article.md'
|
||||
|
||||
expect(shouldIncludeResult(flaw, filePath)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -158,4 +158,41 @@ describe(searchReplace.names.join(' - '), () => {
|
||||
expect(errors[1].lineNumber).toBe(3) // shortTitle: TODOCS
|
||||
expect(errors[2].lineNumber).toBe(4) // intro: TODOCS
|
||||
})
|
||||
|
||||
test('TODOCS placeholder found in documentation about TODOCS usage', async () => {
|
||||
// This test verifies that the TODOCS rule detects instances in documentation files
|
||||
// The actual exclusion happens in the reporting layer, not in the rule itself
|
||||
const markdown = [
|
||||
'---',
|
||||
'title: Using the TODOCS placeholder to leave notes',
|
||||
'shortTitle: Using the TODOCS placeholder',
|
||||
'intro: You can use the `TODOCS` placeholder to indicate work that still needs to be completed.',
|
||||
'---',
|
||||
'',
|
||||
'<!-- markdownlint-disable search-replace -->',
|
||||
'## Using the TODOCS placeholder',
|
||||
'',
|
||||
'To prevent slips, use the string `TODOCS` as your placeholder.',
|
||||
'TODOCS: ADD A SCREENSHOT',
|
||||
'<!-- markdownlint-enable search-replace -->',
|
||||
].join('\n')
|
||||
|
||||
const result = await runRule(searchReplace, {
|
||||
strings: { markdown },
|
||||
config: searchReplaceConfig,
|
||||
markdownlintOptions: { frontMatter: null },
|
||||
})
|
||||
const errors = result.markdown
|
||||
|
||||
// The rule should find TODOCS in frontmatter because markdownlint-disable doesn't apply there
|
||||
// However, since we're testing the actual behavior, let's check what we get
|
||||
const frontmatterErrors = errors.filter((e) => e.lineNumber <= 6)
|
||||
const contentErrors = errors.filter((e) => e.lineNumber > 6)
|
||||
|
||||
// The markdownlint-disable comment should suppress content errors
|
||||
expect(contentErrors.length).toBe(0)
|
||||
|
||||
// Frontmatter errors depend on the configuration - this test documents current behavior
|
||||
expect(frontmatterErrors.length).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user