Skip to content

feat: project level context with rules using it - #1822

Draft
bhirsz wants to merge 4 commits into
mainfrom
feat/project-context
Draft

feat: project level context with rules using it#1822
bhirsz wants to merge 4 commits into
mainfrom
feat/project-context

Conversation

@bhirsz

@bhirsz bhirsz commented Aug 13, 2026

Copy link
Copy Markdown
Member

Summary

Adds project level context to the linter and the first rules built on top of it. This is the foundation discussed in #1677, where the direction was to investigate how robotcode solves this. Implemented self-contained, without depending on the robotcode LSP stack.

Still a draft - more rules are planned on this branch.

Problem

run_project_checks() never parsed anything. It only handed the ConfigManager to each checker, which then had to rglob and parse files on its own. Writing a project rule meant reimplementing discovery, parsing and import resolution every time.

That is why unused-keyword was deprecated and disabled: it defines visit_File, but ProjectChecker has no visiting machinery, so it was effectively dead code.

Project context

A ProjectContext is now built once and passed to ProjectChecker.scan_project:

  • Keyword definitions and usages indexed across the project, with embedded arguments, Resource.Keyword prefixes and BDD prefixes.
  • Argument specifications parsed with Robot Framework's own UserKeywordArgumentParser, so no library is imported and no user code is executed.
  • Keyword visibility resolved through transitive resource imports, so same named keywords in unrelated files are not confused.
  • Variables with Robot Framework compatible scoping (command line > file's own *** Variables *** > imported > builtin).
  • Imports resolved to real paths via robot.variables.Variables.replace_string, so %{ENV}, nesting and escaping behave exactly like in Robot Framework.

Imports using variables that cannot be resolved are marked UNRESOLVABLE and are never reported. This is the main safety valve against false positives on dynamic paths.

The context is only built when an enabled checker actually uses it, so a run without project rules does not pay for parsing the project.

Configuration

Variables can be provided the same way as in Robot Framework:

robocop check-project --variable RESOURCE_DIR:resources
robocop check-project -v RESOURCE_DIR:resources

or in the configuration file:

[tool.robocop.variables]
RESOURCE_DIR = "resources"

Variable names are no longer normalized, so MY-VAR stays MY-VAR. Previously normalize_config_keys rewrote - to _ recursively, which would have silently corrupted user defined variable names.

New rules

All of them are disabled by default and have to be selected explicitly.

invalid-argument-count (ARG08)

Compares the arguments used in a call with the [Arguments] of the definition.

test.robot:19:5 ARG08 Keyword 'Login' expects 2 arguments but 1 provided, missing ${password}
test.robot:20:5 ARG08 Keyword 'Login' expects 2 arguments but 3 provided
test.robot:21:5 ARG08 Keyword 'With Default' expects from 1 to 2 arguments but 0 provided, missing ${a}

Named arguments are resolved the way Robot Framework does it: name=value is only named when name matches an argument of the keyword or the keyword accepts free named arguments, otherwise it is a positional value.

Skipped to avoid false positives: dynamic keyword names, keywords not found or ambiguous, embedded arguments, @{list} / &{dict} expansion, and template keywords.

unused-keyword (KW04)

Un-deprecated and rewritten. Reports keywords never called anywhere in the project, taking into account setups, teardowns, templates and Run Keyword variants.

Private keywords are only matched against calls from their own file. Calls with a name built from a variable become a pattern, so Login ${type} marks both Login Admin and Login User as used.

unused-resource-import (IMP05)

Reports resource imports whose keywords and variables are never used.

test.robot:3:13 IMP05 Imported resource file 'resources/unused.resource' is not used

Resource imports are transitive in Robot Framework, so an import is checked against the usages of every file that can see it. A resource imported only to re-export its own imports is therefore not reported, and neither is a resource that defines no keywords and no variables.

Skipped to avoid false positives: unresolved imports and files calling keywords by a name built from a variable.

duplicated-variable-in-project (DUP11)

Reports the same variable defined in several files that are visible together through imports.

other.resource:2:1 DUP11 Variable 'browser' is also defined in 'common.resource' (line 2)

Robot Framework does not complain about this, but the value used at runtime depends on the import order, which makes it a common source of hard to debug problems. Names are normalized, so ${my var}, ${MY_VAR} and ${myvar} are the same variable.

unresolved-resource-import (IMP07)

Reports resource imports pointing to files that do not exist.

Backwards compatibility

ProjectChecker.scan_project gained an optional third context argument. Custom checkers written against the old two argument signature are detected by inspecting the signature and are still called without the context.

Signature inspection is used rather than try/except TypeError, so a TypeError raised inside a checker is not silently misinterpreted as an old signature.

Docs

The project checks documentation described a scan_project signature and a source argument that never worked. Rewritten and corrected, which closes #1722.

Follow-up

Planned next, not in this PR yet:

Test plan

  • Cross-checked invalid-argument-count against robot --dryrun on a fixture with valid, invalid, nested and dynamic calls - Robocop reports exactly the same set of errors, no more and no less.
  • The previously skipped unused-keyword acceptance tests are enabled again. Expected output gained one keyword defined in a resource file and never called, which is the exact case the old rule could not detect.
  • New unit tests for argument specs and call validation, nested run keyword argument association, keyword visibility through imports, --variable parsing and config key normalization.
  • Manually verified that an unresolvable import variable produces no diagnostic, and that supplying it via --variable makes the import resolve.
  • Full suite: 1969 passed, 77 skipped (was 1883 passed, 79 skipped on main).
  • ruff check / ruff format clean; mypy unchanged from baseline (7 pre-existing errors in untouched files).

Closes #1017
Closes #1119
Closes #1780
Closes #1722

bhirsz and others added 2 commits August 13, 2026 20:59
Introduce `robocop.project`, a package that collects data from the whole
project instead of a single file. It is the foundation for rules that need
to know about other files, such as unused keywords or keyword calls with an
invalid number of arguments.

Previously `run_project_checks()` did not parse anything - it only handed the
`ConfigManager` to every checker, which then had to discover and parse files
on its own. This made project rules impractical to write, which is why the
existing `unused-keyword` rule was deprecated and disabled.

The new `ProjectContext` is built once and passed to `ProjectChecker.scan_project`:

- keyword definitions and usages indexed across the project, with support for
  embedded arguments and `Resource.Keyword` prefixes,
- argument specifications parsed from the AST with `UserKeywordArgumentParser`,
  without importing or executing any code,
- variables with Robot Framework compatible scoping, and imports resolved to
  real paths.

Imports that use variables which cannot be resolved are marked as
`UNRESOLVABLE` and never reported, so dynamic paths do not produce false
positives.

Variables can now be provided with `robocop check-project --variable NAME:VALUE`
(matching `robot --variable`) or in the configuration file:

```toml
[tool.robocop.variables]
RESOURCE_DIR = "resources"
```

Add `unresolved-resource-import` (IMP07) as the first rule using the context.
It is disabled by default and reports resource imports pointing to files that
do not exist.

`ProjectChecker.scan_project` gained an optional `context` argument. Custom
checkers written against the old two argument signature are detected by
inspecting the signature and are still called without the context.

Also fix the project checks documentation, which described a signature and a
`source` argument that never worked.

Closes #1722

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the two flagship project level rules built on top of `ProjectContext`.

`invalid-argument-count` (ARG08) compares the arguments used in a keyword call
with the `[Arguments]` setting of the keyword definition:

    Login    user            # ARG08, expects 2 arguments but 1 provided

Argument specifications are parsed from the AST, so no library is imported and
no user code is executed. Only user keywords defined in the project are checked.
To keep false positives away, the call is skipped when the keyword name is built
from a variable, the keyword is not found or ambiguous, it uses embedded
arguments, the call expands a list or dictionary variable, or the keyword is
used as a test template.

Named arguments are resolved the way Robot Framework does it: `name=value` is
only a named argument when `name` matches an argument of the keyword or the
keyword accepts free named arguments. Otherwise it is a positional value.

The rule was validated against `robot --dryrun` on a fixture covering valid,
invalid and dynamic calls, and reports exactly the same set of errors.

`unused-keyword` (KW04) is no longer deprecated and is rewritten on top of the
project context. Previously it only worked for suites and private keywords and
was dead code, since `ProjectChecker` has no visiting machinery. It now reports
keywords that are not called anywhere in the project, including calls from test
setups, teardowns, templates and keywords nested in `Run Keyword` variants.

Keywords tagged with `robot:private` are only matched against calls from the
file they are defined in. Calls with a name built from a variable are turned
into a pattern, so `Login ${type}` marks both `Login Admin` and `Login User` as
used instead of reporting them.

Both rules are disabled by default and have to be selected explicitly.

Supporting changes:

- `iterate_keyword_calls()` returns each keyword name together with the
  arguments belonging to that particular call. This is required for nested run
  keywords, where the arguments of the statement are not the arguments of the
  called keyword.
- `ProjectContext.visible_keywords()` resolves keywords through transitive
  resource imports instead of a flat project wide index, so same named keywords
  in unrelated files are not confused with each other.
- BDD prefixes are recognized, so `Given The User Logs In` also matches a
  keyword named `The User Logs In`.
- The project context is no longer built when no enabled checker uses it.
  Parsing the whole project on every `check-project` run was wasted work now
  that all project rules are opt-in.

The previously skipped `unused-keyword` acceptance tests are enabled again. Its
expected output gained one keyword, which is defined in a resource file and
never called - the exact case the rule could not detect before.

Closes #1017

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bhirsz bhirsz changed the title feat: add project context for project level rules feat: project level context with unused-keyword and invalid-argument-count rules Aug 13, 2026
…ules

Extend the project context with variable usages so that project level rules
can tell whether anything provided by an import is actually used.

New rules:

- IMP05 `unused-resource-import` reports resource imports whose keywords and
  variables are never used. Resource imports are transitive, so usages are
  looked up in every file that can see the import, and resources providing
  nothing on their own are never reported. Files calling keywords by a name
  built from a variable are skipped to avoid false positives.
- DUP11 `duplicated-variable-in-project` reports the same variable defined in
  multiple files visible together through imports, which makes the value used
  at runtime depend on the import order.

Both rules are disabled by default and run with `robocop check-project`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bhirsz bhirsz changed the title feat: project level context with unused-keyword and invalid-argument-count rules feat: project level context with rules using it Aug 13, 2026
Finding the files that see a resource import walked the transitive import
closure of every file for every checked file. Build the reverse index once
per run instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rule Request: Unused resource ProjectChecker.scan_project() documentation [Rule] Duplicated Variable Name [Rule] Not used keyword

1 participant