Skip to content

Support semantic tokens for format specifiers in logging calls #4003

Open
kavix wants to merge 1 commit into
facebook:mainfrom
kavix:fix-logging-format-specifiers
Open

Support semantic tokens for format specifiers in logging calls #4003
kavix wants to merge 1 commit into
facebook:mainfrom
kavix:fix-logging-format-specifiers

Conversation

@kavix

@kavix kavix commented Jul 1, 2026

Copy link
Copy Markdown

Summary

Fixes #3908

This PR adds a formatSpecifier semantic token for Python logging calls, similar to how clangd highlights format specifiers in C/C++.

Since Ruff flags Python logging f-strings (for lazy evaluation performance reasons via rule G004), users are encouraged to use percent-style formatting. This change ensures that when users follow this best practice, their format specifiers (%s, %d, %(name)s, etc.) are properly syntax-highlighted by the IDE for readability.

Approach & Details

  1. Registered formatSpecifier: Added this custom token type to the SemanticTokensLegends registry.
  2. Detection Heuristic: Introduced a lightweight AST heuristic (is_logger_candidate) to detect common logging functions (e.g. logger.info, logging.debug, LOGGER.warn) to avoid costly symbol resolutions on the hot path.
  3. Format Specifier Parsing: Added a OnceLock cached regex that strictly follows Python's printf-style format specifiers to scan string literals passed to the detected logging calls.
  4. Source Propagation: Updated the process_ast semantic token builder pipeline to propagate the source text string down through the visitor so that string literals can be sliced and matched against the regex.
  5. Testing: Added the semantic_tokens_format_specifier integration test to lsp_interaction which successfully asserts the LSP token mapping over the string "Hello %s %d".

…acebook#3908)

Why:
We want to highlight printf-style format specifiers inside logging strings, similar to how clangd handles this for C/C++. Since Ruff flags Python logging f-strings for lazy evaluation performance reasons, users are encouraged to use percent-style formatting, which should be properly highlighted for readability.

What:
We add a `formatSpecifier` semantic token to the LSP legends. We also introduce a lightweight heuristic to detect common logging functions (e.g. `logger.info`, `logging.debug`) to avoid costly symbol resolutions on the hot path. When a string literal argument is passed to these detected logging calls, we scan the literal against a strict Python format-specifier Regex.

Why it works:
To make the string regex matching work, we need access to the original source text. The `process_ast` pipeline was updated to propagate the module source string all the way down through the AST visitor, allowing us to slice the string accurately using the AST node ranges and extract format specifiers.
@meta-cla meta-cla Bot added the cla signed label Jul 1, 2026
@kavix kavix changed the title Support semantic tokens for format specifiers in logging calls (fixes… Support semantic tokens for format specifiers in logging calls Jul 1, 2026
@github-actions github-actions Bot added the size/l label Jul 1, 2026
@kavix

kavix commented Jul 7, 2026

Copy link
Copy Markdown
Author

Hi @kinto0, could you please review this PR ? Thanks!

@kinto0 kinto0 self-assigned this Jul 7, 2026
@meta-codesync

meta-codesync Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@kinto0 has imported this pull request. If you are a Meta employee, you can view this in D110947960.

@kinto0 kinto0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

hey, thanks for taking this on. left a few suggestions

let disabled_ranges = disabled_ranges_for_module(ast.as_ref(), *handle.sys_info());
let mut builder = SemanticTokenBuilder::new(limit_range, disabled_ranges);

let source = module_info.contents().as_str();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think the source is entirely necessary in processing the ast. can we instead use &ast that we already send?

we should be able to use StringLiteralValue

interaction.shutdown().unwrap();
}

#[test]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

please add a test for %% - I think it will fail

SemanticTokenType::REGEXP,
SemanticTokenType::OPERATOR,
SemanticTokenType::DECORATOR,
SemanticTokenType::new("formatSpecifier"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI is telling me this will allocate on every match of our token. can we avoid this somehow?

Comment on lines +237 to +239
let Expr::Attribute(attr) = func else {
return false;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should we instead make callers filter this?

}
}

fn is_logger_candidate(func: &Expr) -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

docstring here would be nice so we understand what it should do

if start_offset.to_usize() > source.len() || end_offset.to_usize() > source.len() {
return;
}
let text = &source[start_offset.to_usize()..end_offset.to_usize()];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we can just use string_lit.value and avoid source/range entirely here

};

let method = attr.attr.as_str();
if ![

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what makes you want to limit this to logging calls? it looks like #3908 is asking about printf which would not even be covered. can we try filtering in a different way?

"textDocument": { "uri": uri.to_string() }
}))
.expect_response_with(move |response| match response {
Some(SemanticTokensResult::Tokens(tokens)) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

preexisting but this test is very difficult to read. is there any way to make a helper that makes the test boilerplate small and makes it easy to see what we are testing against?

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

highlight for %s in string

2 participants