1
0
mirror of synced 2025-12-19 09:57:42 -05:00

Fix TODOCS placeholder linting exclusion for documentation file (#57551)

This commit is contained in:
Kevin Heis
2025-10-14 09:26:33 -07:00
committed by GitHub
parent f3c832edd2
commit 7511ff33d3
7 changed files with 295 additions and 11 deletions

View File

@@ -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 -->

View 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
}

View File

@@ -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]

View File

@@ -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) {

View File

@@ -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 = {

View 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)
})
})
})

View File

@@ -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)
})
})