diff --git a/.claude/skills/fix-issue/findings/mdl-other.jsonl b/.claude/skills/fix-issue/findings/mdl-other.jsonl index ae4f5d78a..b889534f2 100644 --- a/.claude/skills/fix-issue/findings/mdl-other.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-other.jsonl @@ -103,4 +103,5 @@ {"area": "mdl/linter", "date": "2026-10-07", "symptom": "upstream mendixlabs/mxcli#1217: \"Lint CONV011 (NoCommitInLoop) misses CHANGE … COMMIT (and CREATE … COMMIT) inside a loop; only a separate COMMIT activity is flagged\". `change $T (\"Done\" = true) commit;` in a loop passed lint and `check`; `change …; commit $T;` was flagged. Studio Pro's recommender flags both (MXP004).", "cause": "CONV011 matched only `*microflows.CommitObjectsAction`; the Commit property of CreateObjectAction / ChangeObjectAction was never consulted. MDL-PERF01 (check-time twin, #1186) pinned its boundary to CONV011's, so it inherited the same gap by design.", "file": "`mdl/linter/rules/conv_loop_commit.go` (`committingActionKind`), `mdl/executor/validate_commit_in_loop.go` (ChangeObjectStmt/CreateObjectStmt cases); tests `conv_loop_commit_test.go`, `validate_commit_in_loop_test.go`; example `mdl-examples/bug-tests/1217-commit-clause-in-loop.mdl`", "insight": "**A rule that detects a database effect must enumerate every action that HAS the effect, not the action NAMED after it.** Commit is a property on create/change as well as an activity of its own; grep the action types for a `Commit` field before trusting a commit rule's coverage. Any value other than `No` commits — YesWithoutEvents skips handlers, not the round trip. When two rules share a pinned boundary (CONV011 ↔ MDL-PERF01), a coverage fix lands in both in the same change or they drift. Verified end to end: exec the repro into testdata/expr-checker/minimal.mpr and `lint -r CONV011` — reverted build flags 1 of 3 (only the separate commit), fixed build 3 of 3, list-commit-after-loop control quiet in both.", "refs": ["mendixlabs/mxcli#1217", "mendixlabs/mxcli#1186"], "rules": ["CONV011", "MDL-PERF01"]} {"area": "mdl/exprcheck", "date": "2026-10-07", "symptom": "mendixlabs/mxcli#1216: `declare $D DateTime = parseDateTimeUTC($Text, 'yyyy-MM-dd', empty);` — `mxcli check -p` reports `parseDateTimeUTC() expects 2 argument(s), got 3. [E006]` and exec refuses, while mx check on 11.14.0 reports 0 errors; only --no-check writes it", "cause": "funcTable listed the parse functions without their default-value overloads: parseDateTime/parseDateTimeUTC(value, format [, default]), parseInteger(value [, default]), parseDecimal(value [, format [, default]]) were all fixed at their minimum arity", "file": "`mdl/exprcheck/func_checker.go` (funcTable), test `mdl/exprcheck/parse_default_arity_test.go`, `mdl-examples/bug-tests/1216-parse-datetime-default-value.mdl`", "insight": "funcTable arities are a transcription, so widen each one only by measurement, and measure the siblings the reporter says 'presumably' — two of them (parseInteger/parseDecimal) had the same false E006, but parseBoolean($s, false) is CE0117 and stays 1-arg. Isolate each case in its own project copy: mx check names the activity by its caption ('Create Date and time variable'), so several cases in one project are indistinguishable. Plausible wrong turn: a default of currentDateTime() fails CE0117, which reads as 'the 3-arg UTC form is invalid' — it is currentDateTime() itself, rejected on its own in a microflow; vary the default ($var, empty) before concluding. E006 only fires with -p, so a project-less `mxcli check` of the repro passes and proves nothing.", "ce": ["CE0117"], "rules": ["E006"]} {"area": "mdl/exprcheck", "date": "2026-10-07", "symptom": "`declare $D DateTime = currentDateTime();` passes `mxcli check` and `exec`, then mx check on 11.14.0 fails `[error] [CE0117] \"Error(s) in expression.\" at Create variable activity 'Create Date and time variable'` — in a microflow and a nanoflow alike", "cause": "funcTable listed `currentDateTime` as a zero-argument built-in. Mendix has no such function; the current time is the `[%CurrentDateTime%]` token. funcTable is MDL044's sole allow-list, so the entry silenced the one rule that would have caught it", "file": "`mdl/exprcheck/func_checker.go` (entry removed), `mdl/exprcheck/unknown_funcs.go` (tokenFuncs → FuncRef.Token), `mdl/executor/validate_microflow.go` (MDL044 hint), bug tests `current-datetime-function.fail.mdl` / `current-datetime-token.mdl`", "insight": "Found by accident while measuring #1216: a default of currentDateTime() made parseDateTime look like it rejected a third argument. Before blaming the outer construct, build the inner expression on its own. The removal alone would give a useless hint — nearestFunc offers a spelling match, and the right answer is a token rather than a function — so name the token in the hint. Nothing in the repo emitted or recommended currentDateTime() (every example uses the token), which suggests the entry came from transcription, like the year()/month()/trunc() entries before it. The remaining unverified extraction names (dayOfYear, hour, …) are the same risk.", "ce": ["CE0117"], "rules": ["MDL044"]} +{"area": "mdl/linter", "date": "2026-10-07", "symptom": "A Starlark lint rule cannot tell which project languages are enabled: strings() returns rows for every stored translation (a fresh en_US-only 11.12.5 app already has 124 nl_NL rows), enabling nl_NL changes nothing a rule sees, and a rule calling languages() is skipped with 'undefined: languages (a builtin this mxcli does not have — the rule may need a newer mxcli)'", "cause": "No builtin exposed Settings$LanguageSettings.Languages; LintReader had no GetProjectSettings, so the only language data a rule could reach was the catalog's strings table, which indexes stored translations, not enabled languages", "fix": "languages() builtin returning struct language{code,is_default,check_completeness} from LintReader.GetProjectSettings (added to the interface; the backend already implements it). Classified CatalogFast; a reader error fails the rule instead of answering []", "insight": "Stored translations are not the enabled set: Studio Pro and the starter app keep texts in languages the project never enabled, so any 'per language' check built on strings() over-reports. Project-level facts belong on the reader path (like project_security()), not in the catalog, so they need no FULL build. A new builtin trips three guards at once — builtinModes, the skill's builtin list and its struct table — which is the checklist", "issue": "mendixlabs/mxcli#1306", "file": "mdl/linter/starlark.go (builtinLanguages); mdl/linter/context.go (LintReader)", "test": "mdl/linter/starlark_languages_test.go"} {"area": "mdl/catalog", "date": "2026-10-07", "symptom": "activities_for() / CATALOG.ACTIVITIES returns every call and delete with its target empty: `MicroflowCallAction: action_ref=\"\" entity_ref=\"\" service_ref=\"\"` (also NanoflowCallAction, JavaActionCallAction, JavaScriptActionCallAction, DeleteObjectAction); no column says which call runs in a task queue; a nanoflow's JavaScript action call has no refs_from() row. refs_from() names the call/delete targets fine, and the lint run reports nothing.", "cause": "describeAction (builder_microflows.go) filled ActionRef/EntityRef only for REST/web service/OData calls and create/retrieve; the call and delete cases were never written. The delete's entity was resolvable all along via buildVarEntityMap, which only the refs builder used. microflowActionRef had no JavaScriptActionCallAction case.", "file": "`mdl/catalog/builder_microflows.go` (describeAction, insertFlowActivities), `mdl/catalog/builder_references.go` (microflowActionRef), `mdl/catalog/tables.go` (QueueRef, schema 23)", "insight": "Two builders derive per-action facts from the same parsed actions -- the refs switch and the activities switch -- and drift independently: an action can have its target in refs and an empty column in activities. When a row field is empty, check whether the other switch already resolves it before writing new resolution; here the delete's entity reused buildVarEntityMap (params, create/retrieve outputs, loop iterators) per flow. The queue lives on MicroflowCall.QueueSettings.Queue (and JavaActionCallAction.QueueSettings), not a top-level Queue. A new activities column breaks hand-built `CREATE TABLE activities` fixtures in linter tests (COALESCE does not cover a missing column) and needs a CatalogSchemaVersion bump. Control: HEAD binary on the issue's repro prints exactly the reported empty rows.", "refs": ["mendixlabs/mxcli#1305", "mendixlabs/mxcli#1266", "mendixlabs/mxcli#1267"]} diff --git a/.claude/skills/mendix/write-lint-rules/SKILL.md b/.claude/skills/mendix/write-lint-rules/SKILL.md index 89ca366f5..e1e97919a 100644 --- a/.claude/skills/mendix/write-lint-rules/SKILL.md +++ b/.claude/skills/mendix/write-lint-rules/SKILL.md @@ -80,6 +80,7 @@ Without this, a rule that reads a full-only table under a fast build gets | `module_roles()` | list of module_role | All module roles (deduplicated from role mappings) | | `role_mappings()` | list of role_mapping | User role to module role assignments | | `project_security()` | project_security or None | Project-level security settings (requires MPR reader) | +| `languages()` | list of language | The languages **enabled** in the project settings, in settings order (requires MPR reader; `[]` without one). Use it to scope per-language checks: `strings()` has a row for every stored translation, including languages the project never enabled | | `xpath_expressions()` | list of xpath_expression | All XPath constraint expressions in the catalog (access rules, retrieve actions, widgets) (full catalog — auto-detected) | | `modules()` | list of module | The user's modules (not System, not Marketplace), with their domain model's documentation | | `associations()` | list of association | All non-system associations, same-module and cross-module, with the delete behaviour of both ends | @@ -572,6 +573,16 @@ Returned by `project_security()`. Returns `none` if no MPR reader is available. | `require_mixed_case` | bool | Must contain upper and lower case | | `require_symbol` | bool | Must contain a symbol | +### language + +Returned by `languages()`, one per language enabled in the project settings. A per-language check skips `strings()` rows whose `language` is not among these codes. + +| Property | Type | Description | +|----------|------|-------------| +| `code` | string | Language code: `"en_US"`, `"nl_NL"` | +| `is_default` | bool | Whether this is the project's default language | +| `check_completeness` | bool | Whether Studio Pro checks this language's translations for completeness | + ## Helper Functions | Function | Description | diff --git a/CHANGELOG.md b/CHANGELOG.md index cd3b512da..c609509e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added +- **`languages()` in Starlark lint rules — the languages enabled in the project** (mendixlabs/mxcli#1306) — a rule had no way to tell which languages are enabled: `strings()` has a row for every stored translation, and a fresh en_US-only app already carries nl_NL texts, so enabling a language changed nothing a rule could see, and `languages()` failed to load with `undefined: languages`. It returns one `language` per enabled language, from the project settings, with `code`, `is_default` and `check_completeness`, so a per-language check (every page title translated into each enabled language) can filter `strings()` to the enabled codes. It reads the project settings, not the catalog, so it needs no full catalog build; a failed settings read fails the rule instead of answering `[]`. - **A published REST service's authentication** (mendixlabs/mxcli#1331) — `create [or modify] published rest service M.Api (…, Authentication: (basic, session, microflow M.Authenticate))` sets Studio Pro's **Requires authentication** and its methods: `basic` (username and password), `session` (active session), `microflow M.F` (custom); `Authentication: none` is "Requires authentication: No". The methods are stored in the order written, which is how Studio Pro stores the ones ticked, and `describe` prints them in the stored order (it omits `none`). Left out, `create or modify` and `alter` keep the stored setting, as before. `alter published rest service M.Api set ( Key: value, … )` now takes create's property list (`Path`, `Version`, `ServiceName`, `Authentication`); `set Key = '…'` still works. The authentication microflow must return `System.User` and take only a `System.HttpRequest` and/or `System.HttpResponse` (Mendix: CE0334, CE0336, measured with `mx check` on 11.14.0); `exec` and `check --references` refuse anything else as **MDL-REST04**. Executing the `describe` output of each of ako/TestApp's Studio Pro services writes nothing. - **`send email` — the built-in Send Email activity (Mendix 11.13+, beta)** — `send email ( From: …, To: …, Subject: 'Order {1}' with ({1} = …), Body: template '…', HtmlBody: template '…', Headers: ('X-Name': 'value'), Attachment: $Doc, Host: …, Port: …, SecurityType: ssl, CheckServerIdentity: true, ConnectionTimeout: 30000, Authentication: basic (Username: …, Password: …) ) [on error …];` creates a `Microflows$SendEmailAction`, which sends SMTP mail without the Email Connector module. The settings are one property list (ADR-0013), keyed by the metamodel's names; an unknown, repeated or misshapen key is an error, and so is a missing From, Host, Port or recipient (mxbuild: CE0166). `describe microflow` renders the activity instead of `-- Unsupported action: Microflows$SendEmailAction`, and the description re-executes to the same activity. `check` type-checks its expressions (E009: String addresses, host and credentials, Integer/Long port, String template parameters — what mxbuild reports as CE9528/CE0117), and warns on a server-identity check without SSL and on header names Studio Pro would refuse (MDL-EMAIL02/03). A 10.x–11.12 project is refused; a stored activity MDL cannot restate (authentication document, pre-11.13 message) still describes as unsupported rather than being rewritten smaller. `mxcli syntax microflow.send-email`. (mendixlabs/mxcli#1315) - **`mxcli playwright check` — a text verdict for pages of a running app, in one call** — `mxcli playwright check /p/a /p/b -p app.mpr` loads each page in one headless browser and prints the verdict `run --page-check` prints (title, heading, rows, visible text, error banners, console errors) plus failed same-origin requests and the HTTP status, then `OK n page(s)` or `FAIL k of n page(s)`. **Exit status** 0 all passed, 1 a page failed (HTTP error, sign-in form or a 401 instead of the page, an error banner or error dialog, a console error, a failed request, a failed assertion), 2 the check could not run. It signs in when needed — `--user/--password`, `--role R` (the project's demo user with that user role), or with only `-p` a demo user — saves the session under `.mxcli/playwright-check/`, reuses it on the next check and renews it when the runtime has restarted. `--assert-text`, `--assert-count 'SELECTOR>=N'`, and `--screenshot out.png`, which prints the path and never the image. It replaces the hand-written playwright-cli login/goto/sleep/eval/screenshot sequences that were about a sixth of the tool calls in a measured app-building session, each ending in a PNG read; the `test-app` and `verify-in-runtime` skills and `/mendix:test` now route "check a page" to it. diff --git a/mdl-examples/bug-tests/1306-lint-languages-builtin.mdl b/mdl-examples/bug-tests/1306-lint-languages-builtin.mdl new file mode 100644 index 000000000..7eeb00165 --- /dev/null +++ b/mdl-examples/bug-tests/1306-lint-languages-builtin.mdl @@ -0,0 +1,21 @@ +mdl 1; +-- ============================================================================ +-- mendixlabs/mxcli#1306 — a Starlark lint rule could not read the enabled +-- project languages +-- ============================================================================ +-- +-- Reported: strings() returns rows for every stored translation, including +-- languages that are not enabled (a fresh en_US-only app already carries nl_NL +-- texts), so enabling nl_NL changed nothing a rule could see; a rule calling +-- languages() was skipped with "undefined: languages". +-- +-- The assertion is a Go test (mdl/linter/starlark_languages_test.go): the +-- symptom is in the lint API, which `make check-mdl` does not exercise. This +-- script is the model change from the report. To reproduce by hand on a fresh +-- `mxcli new` app, run it, then lint a rule that returns +-- [violation(message = l.code) for l in languages()] +-- before it, languages() lists en_US only; after it, en_US and nl_NL, while +-- strings() reports the same per-language counts both times. +-- ============================================================================ + +alter settings LANGUAGE add or modify 'nl_NL' (CheckCompleteness: true); diff --git a/mdl/linter/context.go b/mdl/linter/context.go index c1530319e..a3600273f 100644 --- a/mdl/linter/context.go +++ b/mdl/linter/context.go @@ -24,6 +24,7 @@ type LintReader interface { GetMicroflow(id model.ID) (*microflows.Microflow, error) ListMicroflows() ([]*microflows.Microflow, error) GetProjectSecurity() (*security.ProjectSecurity, error) + GetProjectSettings() (*model.ProjectSettings, error) GetNavigation() (*types.NavigationDocument, error) ListPages() ([]*pages.Page, error) ListModules() ([]*model.Module, error) diff --git a/mdl/linter/rules/dataview_layout_grid_test.go b/mdl/linter/rules/dataview_layout_grid_test.go index a65df2c9b..1150503f0 100644 --- a/mdl/linter/rules/dataview_layout_grid_test.go +++ b/mdl/linter/rules/dataview_layout_grid_test.go @@ -134,11 +134,12 @@ func (r rawUnitReader) ListMicroflows() ([]*microflows.Microflow, error) { r func (r rawUnitReader) GetProjectSecurity() (*security.ProjectSecurity, error) { return nil, nil } -func (r rawUnitReader) GetNavigation() (*types.NavigationDocument, error) { return nil, nil } -func (r rawUnitReader) ListPages() ([]*pages.Page, error) { return nil, nil } -func (r rawUnitReader) ListModules() ([]*model.Module, error) { return nil, nil } -func (r rawUnitReader) ListFolders() ([]*types.FolderInfo, error) { return nil, nil } -func (r rawUnitReader) GetRawUnit(id model.ID) (map[string]any, error) { return r.units[id], nil } +func (r rawUnitReader) GetProjectSettings() (*model.ProjectSettings, error) { return nil, nil } +func (r rawUnitReader) GetNavigation() (*types.NavigationDocument, error) { return nil, nil } +func (r rawUnitReader) ListPages() ([]*pages.Page, error) { return nil, nil } +func (r rawUnitReader) ListModules() ([]*model.Module, error) { return nil, nil } +func (r rawUnitReader) ListFolders() ([]*types.FolderInfo, error) { return nil, nil } +func (r rawUnitReader) GetRawUnit(id model.ID) (map[string]any, error) { return r.units[id], nil } func (r rawUnitReader) ListScheduledEvents() ([]*model.ScheduledEvent, error) { return nil, nil } diff --git a/mdl/linter/starlark.go b/mdl/linter/starlark.go index 158b262a2..b2d997b3f 100644 --- a/mdl/linter/starlark.go +++ b/mdl/linter/starlark.go @@ -417,6 +417,7 @@ func (r *StarlarkRule) buildPredeclared() starlark.StringDict { "module_roles": starlark.NewBuiltin("module_roles", r.builtinModuleRoles), "role_mappings": starlark.NewBuiltin("role_mappings", r.builtinRoleMappings), "project_security": starlark.NewBuiltin("project_security", r.builtinProjectSecurity), + "languages": starlark.NewBuiltin("languages", r.builtinLanguages), // XPath / expression analysis "xpath_expressions": starlark.NewBuiltin("xpath_expressions", r.builtinXPathExpressions), @@ -914,6 +915,39 @@ func (r *StarlarkRule) builtinProjectSecurity(_ *starlark.Thread, _ *starlark.Bu }), nil } +// builtinLanguages returns the languages enabled in the project settings +// (mendixlabs/mxcli#1306). strings() cannot answer this: it has a row for every +// stored translation, and a project carries texts in languages it never +// enabled. A failed settings read fails the rule rather than answering [], +// which a per-language rule would report as clean. +func (r *StarlarkRule) builtinLanguages(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + if err := starlark.UnpackArgs(b.Name(), args, kwargs); err != nil { + return nil, err + } + if r.ctx == nil || r.ctx.Reader() == nil { + return starlark.NewList(nil), nil + } + ps, err := r.ctx.Reader().GetProjectSettings() + if err != nil { + return nil, fmt.Errorf("%s: reading project settings: %w", b.Name(), err) + } + if ps == nil || ps.Language == nil { + return starlark.NewList(nil), nil + } + var out []starlark.Value + for _, l := range ps.Language.Languages { + if l.Code == "" { + continue + } + out = append(out, starlarkstruct.FromStringDict(starlark.String("language"), starlark.StringDict{ + "code": starlark.String(l.Code), + "is_default": starlark.Bool(l.Code == ps.Language.DefaultLanguageCode), + "check_completeness": starlark.Bool(l.CheckCompleteness), + })) + } + return starlark.NewList(out), nil +} + // entityToStarlark converts an Entity to a Starlark struct. func entityToStarlark(e Entity) starlark.Value { return starlarkstruct.FromStringDict(starlark.String("entity"), starlark.StringDict{ diff --git a/mdl/linter/starlark_catalog_mode_guard_test.go b/mdl/linter/starlark_catalog_mode_guard_test.go index 7723a3b3b..dbea4304d 100644 --- a/mdl/linter/starlark_catalog_mode_guard_test.go +++ b/mdl/linter/starlark_catalog_mode_guard_test.go @@ -56,6 +56,7 @@ var builtinModes = map[string]builtinProbe{ "module_roles": {CatalogFast, "module_roles()"}, "role_mappings": {CatalogFast, "role_mappings()"}, "project_security": {CatalogFast, "[project_security()]"}, + "languages": {CatalogFast, "languages()"}, "attributes_for": {CatalogFast, "[a for n in ENTITY_NAMES for a in attributes_for(n)]"}, "modules": {CatalogFast, "modules()"}, "associations": {CatalogFast, "associations()"}, diff --git a/mdl/linter/starlark_languages_test.go b/mdl/linter/starlark_languages_test.go new file mode 100644 index 000000000..85ea4a5f5 --- /dev/null +++ b/mdl/linter/starlark_languages_test.go @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 + +package linter_test + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/mendixlabs/mxcli/mdl/catalog" + "github.com/mendixlabs/mxcli/mdl/linter" + "github.com/mendixlabs/mxcli/model" +) + +type settingsReader struct { + minimalReader + ps *model.ProjectSettings + err error +} + +func (r *settingsReader) GetProjectSettings() (*model.ProjectSettings, error) { return r.ps, r.err } + +func emptyCatalog(t *testing.T) *catalog.Catalog { + t.Helper() + cat, err := catalog.NewFromFile(filepath.Join(t.TempDir(), "cat.db")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { cat.Close() }) + return cat +} + +const languagesDumpRule = ` +def check(): + return [violation(message = "%s|%s|%s" % (l.code, l.is_default, l.check_completeness)) for l in languages()] +` + +// mendixlabs/mxcli#1306: a rule could not tell which languages are enabled. +// strings() has a row for every stored translation, and a fresh en_US-only app +// already carries nl_NL texts, so the enabled set has to come from the +// project's language settings, not from the texts. +func TestLanguagesReturnsTheEnabledProjectLanguages(t *testing.T) { + reader := &settingsReader{ps: &model.ProjectSettings{Language: &model.LanguageSettings{ + DefaultLanguageCode: "en_US", + Languages: []model.Language{ + {Code: "en_US"}, + {Code: "nl_NL", CheckCompleteness: true}, + }, + }}} + vs, r := runSrc(t, linter.NewLintContext(emptyCatalog(t), reader), languagesDumpRule) + var got []string + for _, v := range vs { + got = append(got, v.Message) + } + want := []string{"en_US|True|False", "nl_NL|False|True"} + if strings.Join(got, ",") != strings.Join(want, ",") { + t.Errorf("languages() = %v, want %v", got, want) + } + // Settings come from the MPR reader, not a catalog table, so the rule must + // not force a FULL catalog build. + if m := r.RequiredCatalogMode(); m != linter.CatalogFast { + t.Errorf("a rule calling languages() requires %v, want CatalogFast", m) + } +} + +// Control for the default flag: a project whose default is not en_US. +func TestLanguagesMarksTheProjectDefault(t *testing.T) { + reader := &settingsReader{ps: &model.ProjectSettings{Language: &model.LanguageSettings{ + DefaultLanguageCode: "nl_NL", + Languages: []model.Language{{Code: "en_US"}, {Code: "nl_NL"}}, + }}} + vs, _ := runSrc(t, linter.NewLintContext(emptyCatalog(t), reader), languagesDumpRule) + var got []string + for _, v := range vs { + got = append(got, v.Message) + } + want := []string{"en_US|False|False", "nl_NL|True|False"} + if strings.Join(got, ",") != strings.Join(want, ",") { + t.Errorf("languages() = %v, want %v", got, want) + } +} + +func TestLanguagesWithoutSettingsIsEmpty(t *testing.T) { + for name, ctx := range map[string]*linter.LintContext{ + "no reader": linter.NewLintContext(emptyCatalog(t), nil), + "no settings": linter.NewLintContext(emptyCatalog(t), &settingsReader{}), + "no language settings": linter.NewLintContext(emptyCatalog(t), + &settingsReader{ps: &model.ProjectSettings{}}), + } { + if vs, _ := runSrc(t, ctx, languagesDumpRule); len(vs) != 0 { + t.Errorf("%s: languages() returned %d rows, want 0", name, len(vs)) + } + } +} + +// A settings read that fails must fail the rule, not read as "no languages": +// a per-language rule over [] checks nothing and reports clean. +func TestLanguagesSurfacesAReaderError(t *testing.T) { + ctx := linter.NewLintContext(emptyCatalog(t), &settingsReader{err: errors.New("settings unreadable")}) + path := filepath.Join(t.TempDir(), "rule.star") + src := "RULE_ID = \"T1306\"\nRULE_NAME = \"T1306\"\nDESCRIPTION = \"test\"\nCATEGORY = \"quality\"\nSEVERITY = \"info\"\n" + languagesDumpRule + if err := os.WriteFile(path, []byte(src), 0o644); err != nil { + t.Fatal(err) + } + r, err := linter.LoadStarlarkRule(path) + if err != nil { + t.Fatalf("LoadStarlarkRule: %v", err) + } + vs := r.Check(ctx) + if len(vs) != 1 || !strings.Contains(vs[0].Message, "settings unreadable") { + t.Errorf("want one rule-error violation naming the read failure, got %+v", vs) + } +} diff --git a/mdl/linter/starlark_scheduledevents_test.go b/mdl/linter/starlark_scheduledevents_test.go index c4112c5cf..ae1b64b68 100644 --- a/mdl/linter/starlark_scheduledevents_test.go +++ b/mdl/linter/starlark_scheduledevents_test.go @@ -28,6 +28,7 @@ func (m *minimalReader) GetMicroflow(_ model.ID) (*microflows.Microflow, error) } func (m *minimalReader) ListMicroflows() ([]*microflows.Microflow, error) { return nil, nil } func (m *minimalReader) GetProjectSecurity() (*security.ProjectSecurity, error) { return nil, nil } +func (m *minimalReader) GetProjectSettings() (*model.ProjectSettings, error) { return nil, nil } func (m *minimalReader) GetNavigation() (*types.NavigationDocument, error) { return nil, nil } func (m *minimalReader) ListPages() ([]*pages.Page, error) { return nil, nil } func (m *minimalReader) ListModules() ([]*model.Module, error) { return nil, nil }