From d0302ef3dc874c1db42d7741faffd153733e31bd Mon Sep 17 00:00:00 2001 From: David Sanders Date: Tue, 20 May 2025 23:35:41 -0700 Subject: [PATCH] feat(lint-markdown-links): disallow absolute links --- bin/lint-markdown-links.ts | 32 +++++++++++++++++++++--- tests/fixtures/absolute-internal-link.md | 9 +++++++ tests/fixtures/ignorepaths | 1 + tests/lint-roller-markdown-links.spec.ts | 26 ++++++++++++++++++- 4 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/absolute-internal-link.md diff --git a/bin/lint-markdown-links.ts b/bin/lint-markdown-links.ts index b804357..4d72baf 100644 --- a/bin/lint-markdown-links.ts +++ b/bin/lint-markdown-links.ts @@ -67,6 +67,7 @@ async function fetchExternalLink(link: string, checkRedirects = false) { } interface Options { + allowAbsoluteLinks?: boolean; fetchExternalLinks?: boolean; checkRedirects?: boolean; ignoreGlobs?: string[]; @@ -75,7 +76,12 @@ interface Options { async function main( workspaceRoot: string, globs: string[], - { fetchExternalLinks = false, checkRedirects = false, ignoreGlobs = [] }: Options, + { + allowAbsoluteLinks = false, + fetchExternalLinks = false, + checkRedirects = false, + ignoreGlobs = [], + }: Options, ) { const workspace = new DocsWorkspace(workspaceRoot, globs, ignoreGlobs); const parser = new MarkdownParser(); @@ -95,11 +101,17 @@ async function main( try { // Collect diagnostics for all documents in the workspace for (const document of await workspace.getAllMarkdownDocuments()) { + const absoluteLinks = new Set(); + for (let link of await languageService.getDocumentLinks(document, cts.token)) { if (link.target === undefined) { link = (await languageService.resolveDocumentLink(link, cts.token)) ?? link; } + if (!allowAbsoluteLinks && link.data && link.data.source.hrefText.startsWith('/')) { + absoluteLinks.add(link); + } + if ( link.target && link.target.startsWith('http') && @@ -114,7 +126,7 @@ async function main( cts.token, ); - if (diagnostics.length) { + if (diagnostics.length || absoluteLinks.size) { console.log( 'File Location:', path.relative(URI.file(workspace.root).path, URI.parse(document.uri).path), @@ -128,6 +140,14 @@ async function main( ); errors = true; } + + for (const link of absoluteLinks) { + console.log( + `\tAbsolute link on line ${link.range.start.line + 1}:`, + link.data.source.hrefText, + ); + errors = true; + } } } finally { cts.dispose(); @@ -147,8 +167,8 @@ async function main( function parseCommandLine() { const showUsage = (): never => { console.log( - 'Usage: lint-roller-markdown-links [--root ] [-h|--help] [--fetch-external-links] ' + - '[--check-redirects] [--ignore ]', + 'Usage: lint-roller-markdown-links [--root ] [-h|--help] [--allow-absolute-links]' + + '[--fetch-external-links] [--check-redirects] [--ignore ]', ); process.exit(1); }; @@ -157,6 +177,9 @@ function parseCommandLine() { const opts = parseArgs({ allowPositionals: true, options: { + 'allow-absolute-links': { + type: 'boolean', + }, 'fetch-external-links': { type: 'boolean', }, @@ -209,6 +232,7 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) { } main(path.resolve(process.cwd(), opts.root), positionals, { + allowAbsoluteLinks: opts['allow-absolute-links'], fetchExternalLinks: opts['fetch-external-links'], checkRedirects: opts['check-redirects'], ignoreGlobs: opts.ignore, diff --git a/tests/fixtures/absolute-internal-link.md b/tests/fixtures/absolute-internal-link.md new file mode 100644 index 0000000..606de57 --- /dev/null +++ b/tests/fixtures/absolute-internal-link.md @@ -0,0 +1,9 @@ +# Test + +## Target section + +This is the target section + +## Other Section + +Link to [target section](/broken-internal-link.md) diff --git a/tests/fixtures/ignorepaths b/tests/fixtures/ignorepaths index 0d0547a..25113d1 100644 --- a/tests/fixtures/ignorepaths +++ b/tests/fixtures/ignorepaths @@ -1,3 +1,4 @@ +**/absolute-internal-link.md **/broken-{external,internal}-link.md **/broken-cross-file-link.md **/valid-cross-file-link.md diff --git a/tests/lint-roller-markdown-links.spec.ts b/tests/lint-roller-markdown-links.spec.ts index be08723..10be822 100644 --- a/tests/lint-roller-markdown-links.spec.ts +++ b/tests/lint-roller-markdown-links.spec.ts @@ -30,7 +30,7 @@ describe('lint-roller-markdown-links', () => { '--root', FIXTURES_DIR, '--ignore', - '**/{{broken,valid}-*-link.md,*angle-brackets.md,api-history-*.md}', + '**/{{absolute,broken,valid}-*-link.md,*angle-brackets.md,api-history-*.md}', '*.md', ); @@ -42,6 +42,8 @@ describe('lint-roller-markdown-links', () => { '--root', FIXTURES_DIR, '--ignore', + '**/absolute-internal-link.md', + '--ignore', '**/broken-{external,internal}-link.md', '--ignore', '**/{broken,valid}-cross-file-link.md', @@ -153,4 +155,26 @@ describe('lint-roller-markdown-links', () => { expect(status).toEqual(0); }); + + it('should disallow absolute links by default', () => { + const { status, stdout } = runLintMarkdownLinks( + '--root', + FIXTURES_DIR, + 'absolute-internal-link.md', + ); + + expect(stdout).toContain('Absolute link'); + expect(status).toEqual(1); + }); + + it('should allow absolute links by with --allow-absolute-links', () => { + const { status } = runLintMarkdownLinks( + '--root', + FIXTURES_DIR, + 'absolute-internal-link.md', + '--allow-absolute-links', + ); + + expect(status).toEqual(0); + }); });