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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Fixed

- **CSS rule-set folds re-parse** — the AST-anchored fold marker in
`compact_symbol` now carries its closing `*/` for block-comment languages,
so folded CSS rule sets keep a parseable `}` closer. Corpus re-parse
regressions on `/usr/include`, `/usr/lib/go/src`, and `/usr/lib/python3.14`
(55,498 files, 1,879,020 raw-parse-ok slices): 458 → 0.

## [0.3.1] - 2026-08-24

### Security
Expand Down
5 changes: 5 additions & 0 deletions crates/ctx-symbol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,11 @@ fn fold_at_body_node(
out.push_str(parsed.language.comment_prefix());
out.push(' ');
out.push_str(&omit_marker(omitted));
let closer = parsed.language.comment_close();
if !closer.is_empty() {
out.push(' ');
out.push_str(closer);
}
if keep_tail {
out.push_str(nl);
out.push_str(tail);
Expand Down
2 changes: 1 addition & 1 deletion crates/ctx-symbol/tests/degenerate_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ fn assert_deterministic(source: &str, path: &Path) {

/// Every byte range must land inside the original source and slice back out
/// as valid UTF-8.
fn assert_slices_in_bounds<'a>(source: &str, symbols: &'a [Symbol]) {
fn assert_slices_in_bounds(source: &str, symbols: &[Symbol]) {
for s in symbols {
let bytes = source
.as_bytes()
Expand Down
2 changes: 1 addition & 1 deletion crates/ctx-symbol/tests/markup_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ fn markdown_parent_section_compact_passes_through() {
#[test]
fn html_self_closing_elements_are_anchors_too() {
let src = "<img id=\"logo\" src=\"x.png\"/>\n<br><p id=\"tail\">end</p>\n";
let syms = kinds("icons.html", &src);
let syms = kinds("icons.html", src);
let names: Vec<&str> = syms.iter().map(|s| s.0.as_str()).collect();
assert_eq!(names, ["logo", "tail"]);
}
89 changes: 89 additions & 0 deletions crates/ctx-symbol/tests/symbol_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1453,3 +1453,92 @@ fn doc_comment_requires_direct_adjacency() {
let close = symbols.iter().find(|s| s.name == "close").unwrap();
assert_eq!(close.doc_comment.as_deref(), Some("Attached docs."));
}

fn css_path() -> &'static Path {
Path::new("sample.css")
}

#[test]
fn css_rule_set_fold_keeps_block_comment_closed() {
// The fold marker is a block comment in CSS: it must carry its closing
// `*/`, otherwise the kept `}` line is swallowed by the unterminated
// comment and the compact view no longer re-parses.
let src = "
body {
background-color: #f0f0f8;
color: #000000;
}

table.form {
border: 1px solid #333;
padding: 2px;
}
";
let parsed = ctx_symbol::parse(css_path(), src).unwrap();
let symbols = ctx_symbol::extract_symbols(&parsed);
let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"body"), "symbols: {names:?}");
assert!(names.contains(&"table.form"), "symbols: {names:?}");
for sym in &symbols {
let compact = ctx_symbol::compact_symbol(&parsed, sym);
let re = ctx_symbol::parse(css_path(), &compact).unwrap();
assert!(
!re.tree.root_node().has_error(),
"compact must re-parse cleanly: {compact}"
);
assert!(
compact.contains(" lines omitted] */"),
"block-comment marker must be closed: {compact}"
);
assert!(compact.trim_end().ends_with('}'), "closer kept: {compact}");
}
}

#[test]
fn compact_output_is_byte_stable_across_invocations() {
// Byte-stability is sacred: the same input parsed twice must yield the
// exact same compact bytes for every symbol, on every path (AST-anchored
// brace fold, indentation fold, line-heuristic macro fold).
let cases: &[(&'static Path, &str)] = &[
(
c_path(),
"#define LOG(t) \\\n log_write(t)\nint add(int a, int b) {\n int r = a + b;\n return r;\n}\ntypedef struct Point {\n int x;\n int y;\n} Point;\n",
),
(
py_path(),
"class Point:\n def norm(self):\n x = self.x\n y = self.y\n return x * x + y * y\n",
),
(
go_path(),
"type Point struct {\n\tx int\n\ty int\n}\n\nfunc Add(a, b int) int {\n\treturn a + b\n}\n",
),
(
css_path(),
"table.form {\n border: 1px solid #333;\n padding: 2px;\n}\n",
),
(
rust_path(),
"pub fn add(a: i32, b: i32) -> i32 {\n let r = a + b;\n r\n}\n",
),
];
for (path, src) in cases {
let first = {
let parsed = ctx_symbol::parse(path, src).unwrap();
let symbols = ctx_symbol::extract_symbols(&parsed);
symbols
.iter()
.map(|s| ctx_symbol::compact_symbol(&parsed, s))
.collect::<Vec<_>>()
};
assert!(!first.is_empty(), "no symbols for {path:?}");
let second = {
let parsed = ctx_symbol::parse(path, src).unwrap();
let symbols = ctx_symbol::extract_symbols(&parsed);
symbols
.iter()
.map(|s| ctx_symbol::compact_symbol(&parsed, s))
.collect::<Vec<_>>()
};
assert_eq!(first, second, "compact bytes must be stable for {path:?}");
}
}
Loading