Skip to content

Commit 2c0675d

Browse files
chore: distill review conventions into AGENTS.md and CodeRabbit
Add an author-facing "Coding conventions" section to AGENTS.md and mirror it per path in .coderabbit.yaml so contributors get the same feedback up front that a review would raise, and reviews can focus on design. The conventions are distilled from this repository's recurring review remarks and the Nextcloud developer manual's coding standards. CodeRabbit does not read AGENTS.md automatically, so the substance is repeated per path; the two files are meant to be updated together. Vendored code stays reviewable (only bundled/generated output is filtered) so the bot can still look at human-relevant files such as composer.json. Assisted-by: Claude Code:claude-opus-4-8 Signed-off-by: Christoph Wurst <1374172+ChristophWurst@users.noreply.github.com>
1 parent 6d29392 commit 2c0675d

2 files changed

Lines changed: 175 additions & 2 deletions

File tree

.coderabbit.yaml

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,54 @@
11
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
22
# SPDX-License-Identifier: AGPL-3.0-or-later
33
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
4+
#
5+
# The coding conventions themselves live in AGENTS.md, which CodeRabbit detects and
6+
# applies as review criteria automatically. This file only sets review behavior that a
7+
# guideline file cannot express, plus a few reviewer-only rules that are not author
8+
# conventions and would not otherwise be visible from this repository on CI.
9+
10+
language: en-US
11+
12+
# Review voice. Must stay within CodeRabbit's 250-character limit.
13+
tone_instructions: >-
14+
Review like a senior Nextcloud Mail maintainer: friendly, precise, concise;
15+
focus on correctness, tests and maintainability. Prefix optional/style notes
16+
with "nit:" (non-blocking). Do not restate linters or comment on code the diff
17+
does not touch.
18+
419
reviews:
20+
profile: assertive
21+
# Advisory reviews: findings (including critical ones) are posted as comments but
22+
# never block the merge. CodeRabbit has no critical-only gate — enabling the workflow
23+
# would block on every unresolved finding, nits included — so maintainers decide.
24+
request_changes_workflow: false
25+
review_status: false
26+
enable_prompt_for_ai_agents: false
527
auto_review:
628
enabled: false
729
auto_incremental_review: false
830
base_branches:
931
- stable*
10-
enable_prompt_for_ai_agents: false
11-
review_status: false
32+
33+
# Skip only bundled/generated output. Vendored code stays reviewable so the
34+
# rabbit can still look at human-relevant files there (e.g. composer.json).
35+
path_filters:
36+
- "!node_modules/**"
37+
- "!js/**"
38+
- "!**/*.min.js"
39+
- "!composer.lock"
40+
- "!package-lock.json"
41+
- "!**/l10n/**"
42+
43+
# Reviewer-only guidance (not author conventions, so it is not in AGENTS.md).
44+
path_instructions:
45+
- path: "**"
46+
instructions: >-
47+
- Do not raise formatting issues that php-cs-fixer, eslint or stylelint already
48+
enforce (tabs, quotes, semicolons, line length, trailing commas).
49+
50+
- If a diff looks AI/agent-generated (PR template ignored, missing Assisted-by
51+
trailer, uncharacteristically broad or boilerplate scope, or tests that do not
52+
exercise the change), ask the author for the disclosure that CONTRIBUTING.md
53+
requires (an "Assisted-by: <agent>:<model-id>" trailer) and to explain the
54+
change in their own words.

AGENTS.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,131 @@ The diff shows *what* changed and *how*; a comment exists only to capture *why*
7777
- **No multi-line explanatory blocks, AI-style walkthroughs, or section banners.**
7878
- **Remove a stale comment** only when you're already editing that code for the task at hand — no drive-by cleanups.
7979

80+
## Coding conventions
81+
82+
These are the points maintainers raise again and again in review. Following them up
83+
front keeps review focused on design instead of the same recurring notes. CodeRabbit
84+
reads this file automatically and reviews against these conventions, so the bot flags
85+
the same things — but the author should not need the bot to learn them.
86+
87+
### Scope & commits
88+
- **One concern per PR.** Keep unrelated edits, drive-by refactors, code-style churn,
89+
dependency/lock-file bumps and new runtime-version support out of a feature PR — put
90+
them in a separate PR so they stay backportable to stable branches. Small,
91+
single-purpose PRs sail through; large diffs draw change requests.
92+
- **The commit type must match the change** (see `.github/CONTRIBUTING.md`): moving code
93+
with no behavior change is `refactor:`, not `feat:`. Scopes stay broad (`imap`, `ui`).
94+
- **Reuse before reinventing.** Grep for an existing helper, mapper, constant or pattern
95+
and mirror it instead of writing a parallel implementation.
96+
97+
### PHP backend
98+
- **Types over PHPDoc.** Prefer native param/return/property types; drop PHPDoc that only
99+
restates them; keep `@throws` and anything that adds information. Use precise
100+
`psalm-type` array shapes (reuse existing shape definitions) instead of bare `array`.
101+
Handle Psalm's possible-null and
102+
`string|false` (e.g. `file_get_contents`) results — throw a `ServiceException`, don't
103+
cast the failure away. Use constructor property promotion and strict comparison
104+
(`===`, `in_array(..., true)`).
105+
- **Exceptions.** Catch narrowly — never a blanket `\Exception`/`\Throwable` for a
106+
specific failure. When wrapping, pass the original as `$previous` (or rethrow) so the
107+
stack trace survives. Throw domain exceptions; don't leak abstraction-layer exceptions
108+
(`DoesNotExistException`, storage exceptions) out of a service. Keep `@throws` in sync
109+
with the interface contract.
110+
- **Logging.** Inject `LoggerInterface`; log with a meaningful message and
111+
`['exception' => $e]` context at the right level. Log non-critical conditions at
112+
info/debug and return rather than throwing or spamming warnings. Never swallow errors.
113+
- **Dependency injection & layering.** Inject collaborators (incl. OCP services) via the
114+
constructor, not the service locator. Replace `time()` with `ITimeFactory` for
115+
testability. Don't inject request data such as `userId` into services (controllers
116+
only) — pass it as an argument so the service stays usable from background jobs. Keep
117+
controllers thin (request/response only); business logic and DB access belong in
118+
services; keep constants in their owning class.
119+
- **Nextcloud API boundaries.** Never use another app's private `\OCA\OtherApp\*` API — go
120+
through a stable OCP interface. Respect the minimum server version in `appinfo/info.xml`;
121+
guard newer OCP APIs with `method_exists` + a fallback. Store new config via
122+
`IAppConfig` (no dots in new keys). Controllers use `#[NoAdminRequired]` for non-admin
123+
routes (omit it on admin-only ones), HTTP 422 for validation errors, `HTTP::STATUS_*`
124+
constants, and `TrapError` over hand-written try/catch. Keep 400 (client error) vs 404
125+
(not found) intentional. Prefer kebab-case URLs with the id in the path and a single
126+
`resource` route.
127+
- **Controller access control.** Check that the current user owns the resource before
128+
acting on an incoming id — guard against IDOR, don't act on a guessed id. Take a nullable
129+
`?string $userId` from the predefined core services and return 401 when it is null.
130+
131+
### Mappers & entities
132+
- Let mappers propagate `DoesNotExistException` / `OCP\DB\Exception` to the caller — don't
133+
catch not-found inside the mapper. Reuse the `QBMapper` `insert`/`update`/`find` helpers
134+
instead of hand-writing queries.
135+
- Name accessors `findX`/`getX` (not `store`); keep entity `@method` annotations accurate,
136+
including `int|null` for nullable columns; register column types with `addType` in the
137+
constructor.
138+
139+
### Database & migrations
140+
- **Re-runnable:** check column/table existence before changing schema so a partially
141+
failed migration can retry.
142+
- **Version naming:** name the `Version` class after the *next unreleased* minor and bump
143+
`version` in `appinfo/info.xml`, or the migration won't run. (See the DB index dual
144+
pattern for adding indices without a blocking `changeSchema`.)
145+
- **Foreign keys** on referencing columns, with the delete action chosen from the
146+
relationship: cascade delete for rows the parent owns (so account/mailbox deletion leaves
147+
no orphans), `SET NULL` for an optional reference, `RESTRICT` to prevent deletion. Clean up
148+
dangling rows pre-schema.
149+
- **Indexes:** composite column order matters (`[a,b]``[b,a]`) — match the query's
150+
WHERE/ORDER BY; add covering indexes on exactly the filtered/joined columns; make the
151+
index unique when the column is.
152+
- **Portability & size:** sensible column lengths (avoid MariaDB off-row storage), truncate
153+
before writing fixed-width columns, Oracle needs nullable booleans and treats empty
154+
strings as `NULL`, Postgres/Oracle are strict about VARCHAR-vs-INT. Batch huge
155+
UPDATE/DELETE and emit progress. Inline entity constants in migrations (a loaded class
156+
can hold stale values mid-upgrade).
157+
158+
### Performance
159+
- Push filters/limits/cursors into the DB query; scope by `user_id`; use `WHERE ... IN`
160+
to avoid N+1; don't `array_merge` in a loop. Reuse one Horde IMAP client across a bulk
161+
operation instead of reconnecting per message. Stream large result sets via generators
162+
and guard against OOM — users can have 200+ mailboxes. Prefer a local cache over a
163+
distributed one for recomputable values and scope cache keys per user.
164+
165+
### Frontend (Vue / JS)
166+
- **Async:** `async`/`await` with `try`/`catch`, never mixed with `.then`; never `await`
167+
inside `forEach` (use `for...of`); a missing `await` is a real bug. Await sequential
168+
per-item dispatches instead of flooding the backend, and don't fire one request per
169+
list item — push it to the backend or preload via initial state.
170+
- **Structure:** HTTP handling in the service layer, mutations inside store actions,
171+
business logic out of components. Add a loading/disabled state to any control that
172+
triggers an async action so a double click can't fire it twice. Don't make an element
173+
look clickable when its action is unavailable.
174+
- **Style of code:** early returns over nested conditions, named constants over magic
175+
numbers, `const`/`let` never `var`, pure helpers free of side effects and store access.
176+
Sanitize user-controlled values before they reach the DOM. Use `isDarkTheme` from
177+
`@nextcloud/vue`, not `window.matchMedia('(prefers-color-scheme: dark)')`. Follow the
178+
dev-manual naming the linter can't check: multi-word PascalCase component names
179+
(`SettingsView`, not `Settings`), prefixed sub-components, and acronyms with only the
180+
first letter capitalised (`callHttpApi`).
181+
- **CSS:** see [Styling](#styling); keep styles scoped, follow BEM, prefer a modifier class
182+
over manipulating inline style, use grid/spacing/breakpoint CSS variables (no hard-coded
183+
breakpoints), and remove now-unused styles. Avoid `::v-deep`/`!important` into upstream
184+
component internals; where a deep selector is genuinely needed, comment why so it isn't
185+
dropped by mistake.
186+
187+
### Internationalization
188+
- Wrap every user-facing string, **including aria-labels**, in `t('mail', …)`. Use one
189+
string with placeholders — never concatenate translated fragments (translators reorder
190+
words). Use `n('mail', …)` with a `%n` placeholder for counts. Never compare against a
191+
hard-coded English string or use a translated string as a key. No HTML inside a
192+
translation string — translate the plain parts and HTML-encode the inserts.
193+
194+
### Accessibility
195+
- Real anchor (`<a href … target="_blank">`) for links, not a JS click handler, so screen
196+
readers and native middle-click work. Use semantically correct elements and let
197+
`NcButton` inject the required a11y attributes rather than hand-rolling clickable markup.
198+
199+
### Mail-specific gotchas
200+
- **IMAP UIDs are only unique within one mailbox** — never treat them as global
201+
identifiers; key on the database primary key.
202+
- The app already has building blocks (trusted senders, RFC-2822 address parsing,
203+
`IMAPClientFactory`, `Horde_Mail_Rfc822_Identification`) — reuse them.
204+
80205
## Testing
81206

82207
### Unit Tests
@@ -86,6 +211,11 @@ Located in `tests/Unit/` with structure mirroring `lib/`.
86211
- Use **arrange-act-assert** structure with blank lines separating each phase (no literal comments)
87212
- Mock dependencies via `$this->createMock(Interface::class)`
88213
- Setup mocks in `setUp()` for common fixtures
214+
- **Cover the error and edge paths**, not just the happy path (empty input, the throwing branch, both sort orders); new classes and changed logic need tests
215+
- Declare typed fixture properties (`private Foo&MockObject $foo;`) to avoid dynamic-property deprecation warnings; test methods return `void`
216+
- Only mock external collaborators, never the class under test; assert arguments with `->with(...)` and, instead of the removed `withConsecutive`, branch on the argument with `match`/`if` so behavior depends on input, not call order
217+
- Use the same constants in tests as in production code (don't hard-code their values)
218+
- Hand-written stubs of another app's API give false safety — Psalm keeps passing when the upstream API changes, so keep them in sync or avoid them
89219

90220
#### Running Tests
91221
```bash

0 commit comments

Comments
 (0)