-
tests/lint/check-architecture.php<pre>@@ -112,6 +112,22 @@$report($relative, 'endpoint-validate-action', 'RPC endpoint calls validateAction(), call the check directly (e.g. validateDelete()) or check in the endpoint/command');}+ // ->query()/->prepare() (not the parameter-bound ->prepareStatement() wrapper) with a string built by+ // concatenation: same risk as classic SQL injection, whether or not the value comes from user input+ \preg_match_all('~\$(\w+)\s*(?:\.=|=\s*(?:\$\1\s*)?\.)~', $source, $concatMatches);+ $concatenatedVars = \array_unique($concatMatches[1]);+ \preg_match_all('~->(query|prepare)\s*\(\s*([^)]*)~', $source, $sqlCalls, \PREG_SET_ORDER);+ foreach ($sqlCalls as $call) {+ [, $method, $argument] = $call;+ if (\str_contains($argument, '.')) {+ $report($relative, 'sql-concat', "->{$method}() with inline string concatenation, use ->prepareStatement() with bound parameters");+ continue;+ }+ if (\preg_match('~^\$(\w+)~', \trim($argument), $variable) && \in_array($variable[1], $concatenatedVars, true)) {+ $report($relative, 'sql-concat', "->{$method}(\${$variable[1]}) where \${$variable[1]} is built by concatenation, use ->prepareStatement() with bound parameters");+ }+ }+// rows are reloaded through core/grid-views/rows with the grid view parameters of the client: a grid view that takes// such a parameter (an ID of a parent object) must check access to that object, not only that someone is logged inif (
-
tests/lint/check-comments.php<pre>@@ -0,0 +1,219 @@+<?php++/**+ * YoureCom Standard: comment rules (Marcel 21.10.2026, Sitzung). Managed by _dev/standard (apply.php), do not edit here.+ * - templates (.tpl) and SCSS never have a comment ({* *}, <!-- -->, //, /* */) - not even one line.+ * - PHP and TypeScript: a comment is only allowed directly above a class/interface/trait/enum, a function/method or a+ * property, and only one short descriptive line - PHPStan/TSDoc annotation lines (@param, @return, @var, @template,+ * @throws, @inheritDoc, ...) do not count toward that line and may be as many as needed. Anywhere else (inline in a+ * method body, floating, a file header) is a violation.+ * - Every function/method needs either such a comment or #[\Override] (valid for a parent class, an interface or a+ * trait method alike, PHP checks this itself at compile time) - never neither.+ * - _dev/standard itself is exempt (tooling documentation needs more room, Marcel 21.10.2026: "ist mir egal").+ *+ * Known findings that cannot be fixed yet are listed in tests/lint/config.php (`commentsKnown`: relative file => list+ * of rules); entries that no longer match make the check fail, so the list can only shrink.+ *+ * Usage: php tests/lint/check-comments.php+ */++namespace yourecom\standard;++require __DIR__ . '/lib.php';++/** @var array<string, list<string>> $findings relative file => rules */+$findings = [];+/** @var array<string, string> $messages "file|rule" => message */+$messages = [];++$report = static function (string $file, string $rule, string $message) use (&$findings, &$messages): void {+ $findings[$file][] = $rule;+ $messages["{$file}|{$rule}"] ??= "{$file}: [{$rule}] {$message}";+};++/** Strips comment delimiters and asterisks, returns null if the descriptive text (without PHPStan/TSDoc tags) is one line. */+$tooLong = static function (string $text): ?string {+ $lines = \preg_split('~\R~', $text) ?: [];+ $lines = \array_map(static fn (string $line): string => \trim($line, " \t*/"), $lines);+ $lines = \array_values(\array_filter($lines, static fn (string $line): bool => $line !== ''));+ $descriptive = \array_values(\array_filter($lines, static fn (string $line): bool => !\preg_match('~^@[a-zA-Z]~', $line)));+ return \count($descriptive) > 1 ? 'mehr als eine beschreibende Zeile' : null;+};++// an attribute group "#[...]" can itself contain "]" inside strings/nested brackets; conservative bracket counting+$skipAttribute = static function (array $tokens, int $index, int $direction) use (&$skipAttribute): int {+ $open = $direction > 0 ? '[' : ']';+ $close = $direction > 0 ? ']' : '[';+ $depth = 1;+ $i = $index + $direction;+ for ($count = \count($tokens); $i >= 0 && $i < $count; $i += $direction) {+ $text = \is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i];+ $depth += \substr_count($text, $open) - \substr_count($text, $close);+ if ($depth <= 0) {+ return $i + $direction;+ }+ }+ return $i;+};++foreach (files(['files', 'files_wcf'], ['.php']) as $relative => $path) {+ $tokens = \token_get_all((string)\file_get_contents($path));+ $count = \count($tokens);+ $declarationTokens = [\T_CLASS, \T_INTERFACE, \T_TRAIT, \T_ENUM, \T_FUNCTION, \T_PUBLIC, \T_PROTECTED, \T_PRIVATE, \T_VAR, \T_READONLY, \T_CASE, \T_CONST];++ for ($i = 0; $i < $count; $i++) {+ $token = $tokens[$i];+ if (!\is_array($token) || !\in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT], true)) {+ continue;+ }+ $line = $token[2];++ $j = $i + 1;+ while ($j < $count) {+ if (\is_array($tokens[$j]) && $tokens[$j][0] === \T_WHITESPACE) {+ $j++;+ continue;+ }+ if (\is_array($tokens[$j]) && $tokens[$j][0] === \T_ATTRIBUTE) {+ $j = $skipAttribute($tokens, $j, 1);+ continue;+ }+ break;+ }+ $next = $tokens[$j] ?? null;+ $onDeclaration = \is_array($next) && \in_array($next[0], $declarationTokens, true);++ if (!$onDeclaration) {+ $report($relative, 'stray-comment', "Zeile {$line}: Kommentar nicht direkt über Klasse/Funktion/Property, entfernen");+ continue;+ }+ $problem = $tooLong($token[1]);+ if ($problem !== null) {+ $report($relative, 'comment-too-long', "Zeile {$line}: {$problem}, ein kurzer Satz reicht");+ }+ }++ for ($i = 0; $i < $count; $i++) {+ $token = $tokens[$i];+ if (!\is_array($token) || $token[0] !== \T_FUNCTION) {+ continue;+ }+ $line = $token[2];+ $hasOverride = false;+ $hasComment = false;+ $j = $i - 1;+ while ($j >= 0) {+ $previous = $tokens[$j];+ if (\is_array($previous) && \in_array($previous[0], [\T_WHITESPACE, \T_PUBLIC, \T_PROTECTED, \T_PRIVATE, \T_STATIC, \T_ABSTRACT, \T_FINAL], true)) {+ $j--;+ continue;+ }+ if (\is_array($previous) && \in_array($previous[0], [\T_COMMENT, \T_DOC_COMMENT], true)) {+ $hasComment = $tooLong($previous[1]) === null;+ break;+ }+ $text = \is_array($previous) ? $previous[1] : $previous;+ if ($text === ']') {+ $start = $skipAttribute($tokens, $j, -1);+ $group = '';+ for ($k = $start; $k <= $j; $k++) {+ $group .= \is_array($tokens[$k]) ? $tokens[$k][1] : $tokens[$k];+ }+ if (\preg_match('~\\\\?Override\b~', $group)) {+ $hasOverride = true;+ }+ $j = $start - 1;+ continue;+ }+ break;+ }+ if (!$hasOverride && !$hasComment) {+ $report($relative, 'function-needs-comment-or-override', "Zeile {$line}: Funktion ohne Kommentar und ohne #[\\Override]");+ }+ }+}++// TypeScript: no reliable tokenizer available, line-based heuristic (a comment line followed directly by a+// declaration-looking line counts as "on" it; anything else floating or inline is a violation)+foreach (files(['ts'], ['.ts']) as $relative => $path) {+ if (\str_ends_with($relative, '.d.ts')) {+ continue;+ }+ $lines = \preg_split('~\R~', (string)\file_get_contents($path)) ?: [];+ $inBlock = false;+ $blockStartLine = 0;+ $blockLines = [];+ foreach ($lines as $index => $rawLine) {+ $lineNo = $index + 1;+ $trimmed = \trim($rawLine);+ if ($inBlock) {+ $blockLines[] = $trimmed;+ if (\str_contains($trimmed, '*/')) {+ $inBlock = false;+ $after = \trim($lines[$index + 1] ?? '');+ $onDeclaration = (bool)\preg_match('~^(export\s+)?(default\s+)?(public\s+|private\s+|protected\s+|readonly\s+|static\s+|abstract\s+)*(class|interface|function|get\s|set\s|async\s|\w+[?!]?\s*[:(<])~', $after);+ if (!$onDeclaration) {+ $report($relative, 'stray-comment', "Zeile {$blockStartLine}: Kommentar nicht direkt über Klasse/Funktion/Property, entfernen");+ } else {+ $descriptive = \array_values(\array_filter(+ \array_map(static fn (string $line): string => \trim($line, " \t*/"), $blockLines),+ static fn (string $line): bool => $line !== '' && !\preg_match('~^@[a-zA-Z]~', $line),+ ));+ if (\count($descriptive) > 1) {+ $report($relative, 'comment-too-long', "Zeile {$blockStartLine}: mehr als eine beschreibende Zeile, ein kurzer Satz reicht");+ }+ }+ }+ continue;+ }+ if (\str_starts_with($trimmed, '/*')) {+ $inBlock = !\str_contains($trimmed, '*/');+ $blockStartLine = $lineNo;+ $blockLines = [$trimmed];+ if (!$inBlock) {+ $after = \trim($lines[$index + 1] ?? '');+ $onDeclaration = (bool)\preg_match('~^(export\s+)?(default\s+)?(public\s+|private\s+|protected\s+|readonly\s+|static\s+|abstract\s+)*(class|interface|function|get\s|set\s|async\s|\w+[?!]?\s*[:(<])~', $after);+ if (!$onDeclaration) {+ $report($relative, 'stray-comment', "Zeile {$lineNo}: Kommentar nicht direkt über Klasse/Funktion/Property, entfernen");+ }+ }+ continue;+ }+ if (\str_starts_with($trimmed, '//')) {+ $report($relative, 'stray-comment', "Zeile {$lineNo}: `//`-Kommentar, in TS nie erlaubt (nur /** ... */ direkt über Klasse/Funktion/Property)");+ }+ }+}++// templates and SCSS: never a comment, not even one line+foreach (files(templateDirectories(), ['.tpl']) as $relative => $path) {+ $source = (string)\file_get_contents($path);+ if (\preg_match('~\{\*.*?\*\}~s', $source) || \preg_match('~<!--(?!\[if).*?-->~s', $source)) {+ $report($relative, 'template-comment', 'Template-Kommentar ({* *} oder <!-- -->), Templates haben nie Kommentare');+ }+}+foreach (files(['files/style', 'files_wcf/style'], ['.scss']) as $relative => $path) {+ $source = (string)\file_get_contents($path);+ if (\preg_match('~//|/\*~', $source)) {+ $report($relative, 'scss-comment', 'SCSS-Kommentar, SCSS hat nie Kommentare');+ }+}++$known = config()['commentsKnown'] ?? [];+$errors = [];+foreach ($findings as $file => $rules) {+ foreach (\array_unique($rules) as $rule) {+ if (!\in_array($rule, $known[$file] ?? [], true)) {+ $errors[] = $messages["{$file}|{$rule}"];+ }+ }+}+foreach ($known as $file => $rules) {+ foreach ($rules as $rule) {+ if (!\in_array($rule, $findings[$file] ?? [], true)) {+ $errors[] = "tests/lint/config.php: commentsKnown {$file} [{$rule}] no longer applies, remove it";+ }+ }+}++finish($errors, \count($findings) === 0 ? 'keine Kommentar-Verstöße' : \count($known) . ' bekannt');
-
tests/lint/lib.php<pre>@@ -18,6 +18,7 @@* languageIgnore: list<string>,* xsdKnown: array<string, list<string>>,* architectureKnown: array<string, list<string>>,+ * commentsKnown: array<string, list<string>>,* jsBuildDir: string|null,* jsPhrasesDynamic: bool,* }@@ -32,6 +33,7 @@ function config(): array'languageIgnore' => $config['languageIgnore'] ?? [],'xsdKnown' => $config['xsdKnown'] ?? [],'architectureKnown' => $config['architectureKnown'] ?? [],+ 'commentsKnown' => $config['commentsKnown'] ?? [],'jsBuildDir' => $config['jsBuildDir'] ?? null,'jsPhrasesDynamic' => $config['jsPhrasesDynamic'] ?? false,'coreTables' => $config['coreTables'] ?? [],