Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions site-astro/src/notebook-page.html

Large diffs are not rendered by default.

14 changes: 11 additions & 3 deletions src/indexer/extractors/java.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,17 @@ function callsFromMap(calls: Map<string, number>, exclude?: string): CallSite[]
return out;
}

/** Strip generic type parameters: List<String> → List */
function stripGenerics(name: string): string {
return name.replace(/<[^>]*>/g, '').trim();
/** Strip generic type parameters: List<String> → List. Removes the innermost
* `<...>` repeatedly so nesting works — a single `<[^>]*>` pass turned
* `Map<String, List<String>>` into `Map>`, a name that matches no class. */
export function stripGenerics(name: string): string {
let s = name;
let prev: string;
do {
prev = s;
s = s.replace(/<[^<>]*>/g, '');
} while (s !== prev);
return s.trim();
}

/** Extract modifiers from a node's modifier children.
Expand Down
6 changes: 4 additions & 2 deletions src/indexer/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ const MAX_FILE_SIZE = 1_000_000; // 1 MB
*/
function extractSfcScripts(content: string): string | null {
const blocks: string[] = [];
const re = /<script(?:\s[^>]*)?>[\s\S]*?<\/script>/gi;
// HTML parsers accept whitespace and even junk attributes in an end tag
// (`</script >`, `</script\t\n bar>`), hence `(?:\s[^>]*)?`.
const re = /<script(?:\s[^>]*)?>[\s\S]*?<\/script(?:\s[^>]*)?>/gi;
let m: RegExpExecArray | null;
while ((m = re.exec(content)) !== null) {
const inner = m[0]
.replace(/<script(?:\s[^>]*)?>/i, '')
.replace(/<\/script>/i, '');
.replace(/<\/script(?:\s[^>]*)?>/i, '');
blocks.push(inner);
}
return blocks.length > 0 ? blocks.join('\n') : null;
Expand Down
2 changes: 1 addition & 1 deletion src/kb/view-template.ts

Large diffs are not rendered by default.

23 changes: 22 additions & 1 deletion tests/java-ruby.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, it, expect, beforeAll } from 'vitest';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseFile } from '../src/indexer/parser.js';
import { parseJavaContent } from '../src/indexer/extractors/java.js';
import { parseJavaContent, stripGenerics } from '../src/indexer/extractors/java.js';
import { parseRubyContent } from '../src/indexer/extractors/ruby.js';
import { parseKotlinContent } from '../src/indexer/extractors/kotlin.js';
import { ensureParsersReady } from '../src/indexer/extractors/parser-factory.js';
Expand Down Expand Up @@ -113,6 +113,27 @@ describe('java-parser — symbol extraction (direct)', () => {
expect(cls!.extendsName).toBe('AuthService');
});

// CodeQL-driven fix: one `<[^>]*>` pass turned `Map<String, List<String>>` into
// `Map>` — a name that matches no class, so the edge was silently lost.
it('stripGenerics removes NESTED generic arguments', () => {
expect(stripGenerics('List<String>')).toBe('List');
expect(stripGenerics('Map<String, List<String>>')).toBe('Map');
expect(stripGenerics('Foo<A<B<C>>>')).toBe('Foo');
expect(stripGenerics('Outer<T>.Inner')).toBe('Outer.Inner'); // qualified names stay qualified
expect(stripGenerics('Plain')).toBe('Plain');
});

it('extends / implements names survive nested generics', () => {
const src = `
public class Repo extends Base<Map<String, List<String>>> implements Comparable<Repo>, Handler<Map<K, V>> {
public void run() {}
}
`;
const cls = parseJavaContent(src, AUTH_FILE_ID).symbols.find(s => s.name === 'Repo');
expect(cls!.extendsName).toBe('Base');
expect(cls!.implementsNames).toEqual(['Comparable', 'Handler']);
});

it('extracts method symbols from class body', () => {
const src = `
public class AuthService {
Expand Down
17 changes: 17 additions & 0 deletions tests/kb-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,23 @@ describe('kb view — buildViewData', () => {
});
});

describe('kb view — esc (attribute-safe)', () => {
// The template's `esc` feeds double-quoted attributes (data-dir="${esc(path)}").
// Escaping only & < > let a path or title containing `"` break out of the
// attribute (CodeQL js/incomplete-html-attribute-sanitization).
const esc = new Function(`${VIEW_TEMPLATE.match(/const esc = [^\n]*;/)![0]}; return esc;`)() as (s: unknown) => string;

it('escapes quotes so a value cannot close an attribute', () => {
const out = esc(`a" onmouseover="x' y`);
expect(out).not.toMatch(/["']/);
expect(out).toBe('a&quot; onmouseover=&quot;x&#39; y');
});

it('still escapes the text-node characters', () => {
expect(esc('<b>&')).toBe('&lt;b&gt;&amp;');
});
});

describe('kb view — renderViewHtml', () => {
it('injects data and neutralizes </script> so note text cannot break out', () => {
const html = renderViewHtml(VIEW_TEMPLATE, { summary: { total: 1 }, notes: [{ body: 'evil </script> text' }] });
Expand Down
18 changes: 18 additions & 0 deletions tests/parser.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { describe, it, expect, beforeAll } from 'vitest';
import { join, dirname } from 'node:path';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { parseFile } from '../src/indexer/parser.js';
import { parseTsContent } from '../src/indexer/ts-parser.js';
Expand Down Expand Up @@ -1270,3 +1272,19 @@ describe('Groovy / Gradle / Jenkinsfile extractor', () => {
expect(result.exports).not.toContain('no-colons');
});
});

describe('parser — Vue SFC script extraction', () => {
// HTML parsers accept whitespace and junk attributes in an end tag; a script
// block closed that way must not swallow the rest of the file.
it.each(['</script>', '</script >', '</script\t\n bar>'])('ends a script block at %j', async (close) => {
const dir = mkdtempSync(join(tmpdir(), 'coldstart-sfc-'));
try {
const f = join(dir, 'A.vue');
writeFileSync(f, `<template><div/></template>\n<script>\nexport const first = 1;\n${close}\n<script>\nexport const second = 2;\n</script>\n`);
const r = await parseFile(f, 'vue');
expect(r!.exports).toEqual(expect.arrayContaining(['first', 'second']));
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
Loading