Skip to content
Open
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
12 changes: 1 addition & 11 deletions _typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,13 @@ extend-ignore-identifiers-re = [
"prev",
"normalises",
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_typos.toml removed the identifier ignore for inout, but this PR also introduces "inout" into classifications/_universal_rules.json. If typos doesn't treat inout as a valid word/identifier, CI will start failing. Consider keeping inout in extend-ignore-identifiers-re or adding it to allowed words to match the repository's actual vocabulary.

Suggested change
"normalises",
"normalises",
"inout",

Copilot uses AI. Check for mistakes.
"goes",
"Bare",
"inout",
"ba",
"ede",
]

[default.extend-words]
Bare = "Bare"
Supress = "Supress"
teh = "teh"
Teh = "Teh"

[files]
ignore-hidden = false
ignore-files = true
extend-exclude = [
"CHANGELOG.md",
"./CHANGELOG.md",
"/usr/**/*",
"/tmp/**/*",
Comment on lines 38 to 41
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extend-exclude changed from CHANGELOG.md to ./CHANGELOG.md. Depending on typos’ glob semantics, the leading ./ may stop matching and cause the root changelog to be spellchecked unexpectedly. If the intent is to exclude the root file, keep the pattern consistent with other entries (e.g. CHANGELOG.md or **/CHANGELOG.md).

Copilot uses AI. Check for mistakes.
"/**/node_modules/**",
Expand Down
2 changes: 1 addition & 1 deletion classifications/_universal_rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -1189,7 +1189,7 @@
"inner": "syntax_punctuation",
"inner_attribute_item": "syntax_annotation",
"inner_doc_comment_marker": "syntax_literal",
"input": "syntax_keyword",
"inout": "syntax_keyword",
"instance": "syntax_keyword",
"instance_declarations": "definition_type",
"instance_expression": "operation_operator",
Expand Down
7 changes: 2 additions & 5 deletions crates/ast-engine/src/language.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,8 @@ pub trait Language: Clone + std::fmt::Debug + Send + Sync + 'static {
/// Implementors should override this method and return `Some(Self)` when the
/// file type is supported and `None` when it is not.
fn from_path<P: AsRef<Path>>(_path: P) -> Option<Self> {
unimplemented!(
"Language::from_path is not implemented for type `{}`. \
Override Language::from_path for this type if path-based detection is required.",
std::any::type_name::<Self>()
)
// TODO: throw panic here if not implemented properly?
None
}

fn kind_to_id(&self, kind: &str) -> u16;
Expand Down
95 changes: 22 additions & 73 deletions crates/ast-engine/src/replacer/indent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,6 @@ fn get_new_line<C: Content>() -> C::Underlying {
fn get_space<C: Content>() -> C::Underlying {
C::decode_str(" ")[0].clone()
}
fn get_tab<C: Content>() -> C::Underlying {
C::decode_str("\t")[0].clone()
}

const MAX_LOOK_AHEAD: usize = 512;

Expand Down Expand Up @@ -186,16 +183,21 @@ pub fn formatted_slice<'a, C: Content>(
if !slice.contains(&get_new_line::<C>()) {
return Cow::Borrowed(slice);
}
let (indent, is_tab) = get_indent_at_offset_with_tab::<C>(content.get_range(0..start));
Cow::Owned(
indent_lines::<C>(0, &DeindentedExtract::MultiLine(slice, indent), is_tab).into_owned(),
indent_lines::<C>(
0,
&DeindentedExtract::MultiLine(
slice,
get_indent_at_offset::<C>(content.get_range(0..start)),
),
)
.into_owned(),
)
}

pub fn indent_lines<'a, C: Content>(
indent: usize,
extract: &'a DeindentedExtract<'a, C>,
is_tab: bool,
) -> Cow<'a, [C::Underlying]> {
use DeindentedExtract::{MultiLine, SingleLine};
let (lines, original_indent) = match extract {
Expand All @@ -211,27 +213,18 @@ pub fn indent_lines<'a, C: Content>(
Ordering::Less => Cow::Owned(indent_lines_impl::<C, _>(
indent - original_indent,
lines.split(|b| *b == get_new_line::<C>()),
is_tab,
)),
}
}

fn indent_lines_impl<'a, C, Lines>(
indent: usize,
mut lines: Lines,
is_tab: bool,
) -> Vec<C::Underlying>
fn indent_lines_impl<'a, C, Lines>(indent: usize, mut lines: Lines) -> Vec<C::Underlying>
where
C: Content + 'a,
Lines: Iterator<Item = &'a [C::Underlying]>,
{
let mut ret = vec![];
let indent_char = if is_tab {
get_tab::<C>()
} else {
get_space::<C>()
};
let leading: Vec<_> = std::iter::repeat_n(indent_char, indent).collect();
let space = get_space::<C>();
let leading: Vec<_> = std::iter::repeat_n(space, indent).collect();
// first line wasn't indented, so we don't add leading spaces
if let Some(line) = lines.next() {
ret.extend(line.iter().cloned());
Expand All @@ -248,62 +241,40 @@ where
/// returns 0 if no indent is found before the offset
/// either truly no indent exists, or the offset is in a long line
pub fn get_indent_at_offset<C: Content>(src: &[C::Underlying]) -> usize {
get_indent_at_offset_with_tab::<C>(src).0
}

/// returns (indent, `is_tab`)
pub fn get_indent_at_offset_with_tab<C: Content>(src: &[C::Underlying]) -> (usize, bool) {
let lookahead = src.len().max(MAX_LOOK_AHEAD) - MAX_LOOK_AHEAD;

let mut indent = 0;
let mut is_tab = false;
let new_line = get_new_line::<C>();
let space = get_space::<C>();
let tab = get_tab::<C>();
// TODO: support TAB. only whitespace is supported now
for c in src[lookahead..].iter().rev() {
if *c == new_line {
return (indent, is_tab);
return indent;
}
if *c == space {
indent += 1;
} else if *c == tab {
indent += 1;
is_tab = true;
} else {
indent = 0;
is_tab = false;
}
}
// lookahead == 0 means we have indentation at first line.
if lookahead == 0 && indent != 0 {
(indent, is_tab)
indent
} else {
(0, false)
0
}
}

// NOTE: we assume input is well indented.
// following lines should have fewer indentations than initial line
fn remove_indent<C: Content>(indent: usize, src: &[C::Underlying]) -> Vec<C::Underlying> {
let indentation: Vec<_> = std::iter::repeat_n(get_space::<C>(), indent).collect();
let new_line = get_new_line::<C>();
let space = get_space::<C>();
let tab = get_tab::<C>();
let lines: Vec<_> = src
.split(|b| *b == new_line)
.map(|line| {
let mut stripped = line;
let mut count = 0;
while count < indent {
if let Some(rest) = stripped.strip_prefix(std::slice::from_ref(&space)) {
stripped = rest;
} else if let Some(rest) = stripped.strip_prefix(std::slice::from_ref(&tab)) {
stripped = rest;
} else {
break;
}
count += 1;
}
stripped
.map(|line| match line.strip_prefix(&*indentation) {
Some(stripped) => stripped,
Comment on lines 243 to +276
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change removes tab-indentation support: indent_lines_impl now always inserts spaces, get_indent_at_offset ignores tabs, and remove_indent only strips a space prefix. That will produce incorrect replacements for tab-indented source files. Either restore tab handling (and the removed tab tests) or clearly document/enforce that replacements only support space indentation.

Copilot uses AI. Check for mistakes.
None => line,
})
.collect();
lines.join(&new_line).clone()
Expand All @@ -328,7 +299,7 @@ mod test {
.count();
let end = source.chars().count() - trailing_white;
let extracted = extract_with_deindent(&source, start..end);
let result_bytes = indent_lines::<String>(0, &extracted, source.contains('\t'));
let result_bytes = indent_lines::<String>(0, &extracted);
let actual = std::str::from_utf8(&result_bytes).unwrap();
assert_eq!(actual, expected);
}
Expand Down Expand Up @@ -420,8 +391,8 @@ pass
fn test_replace_with_indent(target: &str, start: usize, inserted: &str) -> String {
let target = target.to_string();
let replace_lines = DeindentedExtract::MultiLine(inserted.as_bytes(), 0);
let (indent, is_tab) = get_indent_at_offset_with_tab::<String>(&target.as_bytes()[..start]);
let ret = indent_lines::<String>(indent, &replace_lines, is_tab);
let indent = get_indent_at_offset::<String>(&target.as_bytes()[..start]);
let ret = indent_lines::<String>(indent, &replace_lines);
String::from_utf8(ret.to_vec()).unwrap()
}

Expand Down Expand Up @@ -474,26 +445,4 @@ pass
let actual = test_replace_with_indent(target, 6, inserted);
assert_eq!(actual, "def abc():\n pass");
}

#[test]
fn test_tab_indent() {
let src = "\n\t\tdef test():\n\t\t\tpass";
let expected = "def test():\n\tpass";
test_deindent(src, expected, 0);
}

#[test]
fn test_tab_replace() {
let target = "\t\t";
let inserted = "def abc(): pass";
let actual = test_replace_with_indent(target, 2, inserted);
assert_eq!(actual, "def abc(): pass");
let inserted = "def abc():\n\tpass";
let actual = test_replace_with_indent(target, 2, inserted);
assert_eq!(actual, "def abc():\n\t\t\tpass");

let target = "\t\tdef abc():\n\t\t\t";
let actual = test_replace_with_indent(target, 14, inserted);
assert_eq!(actual, "def abc():\n\t\tpass");
}
}
23 changes: 10 additions & 13 deletions crates/ast-engine/src/replacer/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT

use super::indent::{DeindentedExtract, extract_with_deindent, indent_lines};
use super::indent::{DeindentedExtract, extract_with_deindent, get_indent_at_offset, indent_lines};
use super::{MetaVarExtract, Replacer, split_first_meta_var};
use crate::NodeMatch;
use crate::language::Language;
Expand Down Expand Up @@ -52,10 +52,10 @@ impl TemplateFix {
impl<D: Doc> Replacer<D> for TemplateFix {
fn generate_replacement(&self, nm: &NodeMatch<'_, D>) -> Underlying<D> {
let leading = nm.get_doc().get_source().get_range(0..nm.range().start);
let (indent, is_tab) = super::indent::get_indent_at_offset_with_tab::<D::Source>(leading);
let indent = get_indent_at_offset::<D::Source>(leading);
let bytes = replace_fixer(self, nm.get_env());
let replaced = DeindentedExtract::MultiLine(&bytes, 0);
indent_lines::<D::Source>(indent, &replaced, is_tab).to_vec()
indent_lines::<D::Source>(indent, &replaced).to_vec()
}
}

Expand All @@ -64,7 +64,7 @@ type Indent = usize;
#[derive(Debug, Clone)]
pub struct Template {
fragments: Vec<String>,
vars: Vec<(MetaVarExtract, Indent, bool)>, // the third element is is_tab
vars: Vec<(MetaVarExtract, Indent)>,
}

fn create_template(
Expand All @@ -82,10 +82,8 @@ fn create_template(
{
fragments.push(tmpl[len..len + offset + i].to_string());
// NB we have to count ident of the full string
let (indent, is_tab) = super::indent::get_indent_at_offset_with_tab::<String>(
&tmpl.as_bytes()[..len + offset + i],
);
vars.push((meta_var, indent, is_tab));
let indent = get_indent_at_offset::<String>(&tmpl.as_bytes()[..len + offset + i]);
vars.push((meta_var, indent));
len += skipped + offset + i;
offset = 0;
continue;
Expand Down Expand Up @@ -115,8 +113,8 @@ fn replace_fixer<D: Doc>(fixer: &TemplateFix, env: &MetaVarEnv<'_, D>) -> Underl
if let Some(frag) = frags.next() {
ret.extend_from_slice(&D::Source::decode_str(frag));
}
for ((var, indent, is_tab), frag) in vars.zip(frags) {
if let Some(bytes) = maybe_get_var(env, var, indent.to_owned(), is_tab.to_owned()) {
for ((var, indent), frag) in vars.zip(frags) {
if let Some(bytes) = maybe_get_var(env, var, indent.to_owned()) {
ret.extend_from_slice(&bytes);
}
ret.extend_from_slice(&D::Source::decode_str(frag));
Expand All @@ -128,7 +126,6 @@ fn maybe_get_var<'e, 't, C, D>(
env: &'e MetaVarEnv<'t, D>,
var: &MetaVarExtract,
indent: usize,
is_tab: bool,
) -> Option<Cow<'e, [C::Underlying]>>
where
C: Content + 'e,
Expand All @@ -139,7 +136,7 @@ where
// transformed source does not have range, directly return bytes
let source = env.get_transformed(name)?;
let de_intended = DeindentedExtract::MultiLine(source, 0);
let bytes = indent_lines::<D::Source>(indent, &de_intended, is_tab);
let bytes = indent_lines::<D::Source>(indent, &de_intended);
return Some(Cow::Owned(bytes.into()));
}
MetaVarExtract::Single(name) => {
Expand All @@ -163,7 +160,7 @@ where
}
};
let extracted = extract_with_deindent(source, range);
let bytes = indent_lines::<D::Source>(indent, &extracted, is_tab);
let bytes = indent_lines::<D::Source>(indent, &extracted);
Some(Cow::Owned(bytes.into()))
}

Expand Down
30 changes: 15 additions & 15 deletions crates/flow/src/incremental/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,21 +471,21 @@ impl IncrementalAnalyzer {
}

// Save edges to storage in batch
if !edges_to_save.is_empty()
&& let Err(e) = self.storage.save_edges_batch(&edges_to_save).await
{
warn!(
error = %e,
"batch save failed, falling back to individual saves"
);
for edge in &edges_to_save {
if let Err(e) = self.storage.save_edge(edge).await {
warn!(
file_from = ?edge.from,
file_to = ?edge.to,
error = %e,
"failed to save edge individually"
);
if !edges_to_save.is_empty() {
if let Err(e) = self.storage.save_edges_batch(&edges_to_save).await {
warn!(
error = %e,
"batch save failed, falling back to individual saves"
);
for edge in &edges_to_save {
if let Err(e) = self.storage.save_edge(edge).await {
warn!(
file_from = ?edge.from,
file_to = ?edge.to,
error = %e,
"failed to save edge individually"
);
}
}
}
}
Expand Down
9 changes: 3 additions & 6 deletions crates/language/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1721,23 +1721,20 @@ pub fn from_extension(path: &Path) -> Option<SupportLang> {
}

// Handle extensionless files or files with unknown extensions
if let Some(_file_name) = path.file_name().and_then(|n| n.to_str()) {
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
// 1. Check if the full filename matches a known extension (e.g. .bashrc)
#[cfg(any(feature = "bash", feature = "all-parsers"))]
if constants::BASH_EXTS.contains(&_file_name) {
if constants::BASH_EXTS.contains(&file_name) {
return Some(SupportLang::Bash);
}

// 2. Check known extensionless file names
#[cfg(any(feature = "bash", feature = "all-parsers", feature = "ruby"))]
for (name, lang) in constants::LANG_RELATIONSHIPS_WITH_NO_EXTENSION {
if *name == _file_name {
if *name == file_name {
return Some(*lang);
}
}

// Silence unused variable warning if bash and ruby and all-parsers are not enabled
let _ = file_name;
}

// 3. Try shebang check as last resort
Expand Down
Loading
Loading