Skip to content

Repo sync #39479

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 23, 2025
Prev Previous commit
Next Next commit
Fix circular links in CodeQL CLI manual pages (#56233)
  • Loading branch information
heiskr authored Jul 23, 2025
commit 8528155e468db2203505aed7488b00ed834075e1
2 changes: 1 addition & 1 deletion src/audit-logs/tests/unit/filter-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { describe, expect, test } from 'vitest'
import { filterByAllowlistValues, filterAndUpdateGhesDataByAllowlistValues } from '../../lib'
import type { RawAuditLogEventT, VersionedAuditLogData } from '../../types'

describe('audit log event fitering', () => {
describe('audit log event filtering', () => {
test('matches single allowlist value', async () => {
const eventsToProcess: RawAuditLogEventT[] = [
{
Expand Down
25 changes: 19 additions & 6 deletions src/codeql-cli/scripts/convert-markdown-for-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ const END_SECTION = '\n:::'
const PROGRAM_SECTION = '::: {.program}\n'

// Updates several properties of the Markdown file using the AST
export async function convertContentToDocs(content, frontmatterDefaults = {}) {
export async function convertContentToDocs(
content,
frontmatterDefaults = {},
currentFileName = '',
) {
const ast = fromMarkdown(content)

let depth = 0
Expand Down Expand Up @@ -160,11 +164,20 @@ export async function convertContentToDocs(content, frontmatterDefaults = {}) {

// Remove the string {.interpreted-text role="doc"} from this node
node.value = node.value.replace(/\n/g, ' ').replace('{.interpreted-text role="doc"}', '')
// Make the previous sibling node a link
link.type = 'link'
link.url = `${RELATIVE_LINK_PATH}/${linkPath}`
link.children = [{ type: 'text', value: linkText }]
delete link.value

// Check for circular links - if the link points to the same file we're processing
const currentFileBaseName = currentFileName.replace('.md', '')
if (currentFileBaseName && linkPath === currentFileBaseName) {
// Convert circular link to plain text instead of creating a link
link.type = 'text'
link.value = linkText
} else {
// Make the previous sibling node a link
link.type = 'link'
link.url = `${RELATIVE_LINK_PATH}/${linkPath}`
link.children = [{ type: 'text', value: linkText }]
delete link.value
}
}

// Save any nodes that contain aka.ms links so we can convert them later
Expand Down
7 changes: 6 additions & 1 deletion src/codeql-cli/scripts/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ async function main() {
matchHeading,
matchHeading + '\n### Primary Options\n',
)
const { data, content } = await convertContentToDocs(primaryHeadingSourceContent)
const currentFileName = path.basename(file)
const { data, content } = await convertContentToDocs(
primaryHeadingSourceContent,
{},
currentFileName,
)
await writeFile(file, matter.stringify(content, data))
const targetFilename = path.join(targetDirectory, path.basename(file))
const sourceData = { ...data, ...frontmatterDefaults }
Expand Down
89 changes: 89 additions & 0 deletions src/codeql-cli/tests/convert-markdown-for-docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { describe, expect, test } from 'vitest'
import { convertContentToDocs } from '../scripts/convert-markdown-for-docs'

describe('convertContentToDocs circular link handling', () => {
const testContent = `
# bqrs interpret

[Plumbing] Interpret data in a single BQRS.

## Description

A command that interprets a single BQRS file according to the provided
metadata and generates output in the specified format.

## Options

### Primary Options

This option has no effect when passed to \`codeql bqrs interpret<bqrs-interpret>\`{.interpreted-text role="doc"}.

For more information, see \`codeql database analyze<database-analyze>\`{.interpreted-text role="doc"}.
`

test('converts circular links to plain text', async () => {
const result = await convertContentToDocs(testContent, {}, 'bqrs-interpret.md')

// Should not contain circular link
expect(result.content).not.toContain(
'[codeql bqrs interpret](/code-security/codeql-cli/codeql-cli-manual/bqrs-interpret)',
)

// Should contain plain text instead
expect(result.content).toContain('codeql bqrs interpret')
})

test('preserves non-circular links', async () => {
const result = await convertContentToDocs(testContent, {}, 'bqrs-interpret.md')

// Should preserve valid cross-reference link
expect(result.content).toContain(
'[codeql database analyze](/code-security/codeql-cli/codeql-cli-manual/database-analyze)',
)
})

test('handles edge case: no filename provided', async () => {
const result = await convertContentToDocs(testContent, {}, '')

// Should preserve link when no filename is provided
expect(result.content).toContain(
'[codeql bqrs interpret](/code-security/codeql-cli/codeql-cli-manual/bqrs-interpret)',
)
})

test('handles edge case: different filename', async () => {
const result = await convertContentToDocs(testContent, {}, 'different-file.md')

// Should preserve link when filename is different
expect(result.content).toContain(
'[codeql bqrs interpret](/code-security/codeql-cli/codeql-cli-manual/bqrs-interpret)',
)
})

test('processes both circular and non-circular links correctly in same content', async () => {
const result = await convertContentToDocs(testContent, {}, 'bqrs-interpret.md')

// Circular link should be plain text
expect(result.content).not.toContain(
'[codeql bqrs interpret](/code-security/codeql-cli/codeql-cli-manual/bqrs-interpret)',
)

// Non-circular link should be preserved
expect(result.content).toContain(
'[codeql database analyze](/code-security/codeql-cli/codeql-cli-manual/database-analyze)',
)

// Both should have their text content present
expect(result.content).toContain('codeql bqrs interpret')
expect(result.content).toContain('codeql database analyze')
})

test('returns proper data structure', async () => {
const result = await convertContentToDocs(testContent, {}, 'bqrs-interpret.md')

expect(result).toHaveProperty('content')
expect(result).toHaveProperty('data')
expect(typeof result.content).toBe('string')
expect(typeof result.data).toBe('object')
})
})
112 changes: 112 additions & 0 deletions src/codeql-cli/tests/test-circular-links.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { convertContentToDocs } from '../scripts/convert-markdown-for-docs'

// Test content that simulates a circular link scenario
const testContent = `
# bqrs interpret

[Plumbing] Interpret data in a single BQRS.

## Description

A command that interprets a single BQRS file according to the provided
metadata and generates output in the specified format.

## Options

### Primary Options

This option has no effect when passed to \`codeql bqrs interpret<bqrs-interpret>\`{.interpreted-text role="doc"}.

For more information, see \`codeql database analyze<database-analyze>\`{.interpreted-text role="doc"}.
`

async function testCircularLinkFix() {
console.log('Testing circular link fix...')

try {
// Test with circular link (should convert to plain text)
const result1 = await convertContentToDocs(testContent, {}, 'bqrs-interpret.md')
console.log('✅ Conversion completed successfully')

// Check if circular link was converted to plain text
const hasCircularLink = result1.content.includes(
'[codeql bqrs interpret](/code-security/codeql-cli/codeql-cli-manual/bqrs-interpret)',
)
const hasPlainText = result1.content.includes('codeql bqrs interpret')

if (hasCircularLink) {
console.log('❌ FAIL: Circular link still present in output')
console.log('Content:', result1.content)
return false
} else if (hasPlainText) {
console.log('✅ PASS: Circular link converted to plain text')
} else {
console.log('⚠️ WARNING: Could not find expected text in output')
}

// Check if non-circular link is preserved
const hasValidLink = result1.content.includes(
'[codeql database analyze](/code-security/codeql-cli/codeql-cli-manual/database-analyze)',
)

if (hasValidLink) {
console.log('✅ PASS: Non-circular link preserved correctly')
} else {
console.log('❌ FAIL: Valid cross-reference link was incorrectly removed')
}

console.log('\n--- Generated content preview ---')
console.log(result1.content.substring(0, 800) + '...')

return !hasCircularLink && hasValidLink
} catch (error) {
console.error('❌ Test failed with error:', error)
return false
}
}

async function testEdgeCases() {
console.log('\nTesting edge cases...')

// Test with no filename (should not crash)
const result1 = await convertContentToDocs(testContent, {}, '')
const hasLink1 = result1.content.includes(
'[codeql bqrs interpret](/code-security/codeql-cli/codeql-cli-manual/bqrs-interpret)',
)
if (hasLink1) {
console.log('✅ PASS: No filename provided - link preserved as expected')
} else {
console.log('❌ FAIL: Link incorrectly removed when no filename provided')
return false
}

// Test with different filename (should preserve link)
const result2 = await convertContentToDocs(testContent, {}, 'different-file.md')
const hasLink2 = result2.content.includes(
'[codeql bqrs interpret](/code-security/codeql-cli/codeql-cli-manual/bqrs-interpret)',
)
if (hasLink2) {
console.log('✅ PASS: Different filename - link preserved correctly')
} else {
console.log('❌ FAIL: Link incorrectly removed for different filename')
return false
}

return true
}

// Run all tests
async function runAllTests() {
const test1 = await testCircularLinkFix()
const test2 = await testEdgeCases()

if (test1 && test2) {
console.log('\n🎉 All tests passed!')
process.exit(0)
} else {
console.log('\n💥 Tests failed!')
process.exit(1)
}
}

runAllTests()
2 changes: 1 addition & 1 deletion src/frame/lib/get-remote-json.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default async function getRemoteJSON(url, config) {
}
}
} catch (error) {
if (!(error instanceof SyntaxError || error.code === 'ENOENT')) {
if (!(error instanceof SyntaxError || (error instanceof Error && error.code === 'ENOENT'))) {
throw error
}
}
Expand Down