Enforce project global ownership in verification and merging

This commit is contained in:
ragnarok-clanker 2026-10-26 20:14:59 +01:00
commit 0e3648a174
8 changed files with 447 additions and 2 deletions

91
test/globals.test.js Normal file
View file

@ -0,0 +1,91 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { newVariables, scanVariables, definesVariable, globalViolations } from '../src/server/globals.js';
const rules = (files, options = {}) => globalViolations(files, options).map((e) => `${e.name}:${e.rule}`).sort();
test('an extern needs a definition, not a symbol-table entry, another extern, or a local namesake', () => {
assert.deepEqual(rules({ 'a.h': 'extern int g_lost;' }, { known: () => true }), ['g_lost:undefined-extern']);
assert.deepEqual(rules({ 'a.h': 'extern int g_lost;' }, { repository: { 'b.h': 'extern int g_lost;', 'b.cpp': 'void f() { int g_lost; }' } }), ['g_lost:undefined-extern']);
assert.deepEqual(rules({ 'a.h': 'extern int g_owned;', 'a.cpp': '// GLOBAL: gl_00001\nint g_owned;' }), []);
assert.deepEqual(rules({ 'a.h': 'extern int g_owned;' }, { repository: { 'a.cpp': '// GLOBAL: gl_00001\nint g_owned;' } }), []);
});
test('a static or differently scoped definition cannot satisfy an external declaration', () => {
const repository = { 'a.cpp': 'static int count; namespace A { int value; }' };
assert.deepEqual(rules({ 'a.h': 'extern int count; extern int value;' }, { repository }), ['count:undefined-extern', 'value:undefined-extern']);
assert.deepEqual(rules({ 'a.h': 'namespace A { extern int value; }' }, { repository }), []);
assert.equal(definesVariable('namespace A { int value; }', 'A::value'), true);
assert.equal(definesVariable('namespace A { int value; }', 'value'), false);
});
test('local statics, namespace storage, class-static definitions and mutable pointers need IDs', () => {
const text = [
'int g_count;',
'// GLOBAL: gl_00002',
'int g_annotated = 0;',
'constexpr int kLimit = 4;',
'const int kMax = 16;',
'const char* g_name = "x";',
'namespace {',
' float s_scale;',
'}',
'class C {',
' int m_field;',
' static int s_shared;',
'};',
'int C::s_shared;',
'void f() {',
' int local = 0;',
' static int s_calls;',
'}',
].join('\n');
assert.deepEqual(rules({ 'a.cpp': text }), ['C::s_shared:unannotated-global', 'f::s_calls:unannotated-global', 'g_count:unannotated-global', 'g_name:unannotated-global', 's_scale:unannotated-global']);
assert.deepEqual(newVariables(text, null).filter((v) => v.member).map((v) => v.name), ['C::s_shared']);
});
test('multiline tables and function-pointer declarations are checked', () => {
const declarations = 'extern int\n (WSAAPI* g_receive)(int, char*, int, int);\nextern const int values[2];';
assert.deepEqual(rules({ 'a.h': declarations }), ['g_receive:undefined-extern', 'values:undefined-extern']);
const definitions = '// GLOBAL: gl_00003\nint(WSAAPI* g_receive)(int, char*, int, int);\n// GLOBAL: gl_00004\nconst int values[2] = {\n 1, 2\n};';
assert.deepEqual(rules({ 'a.h': declarations, 'a.cpp': definitions }), []);
assert.deepEqual(rules({ 'a.cpp': 'static int table[] = {\n 1, 2,\n};\n' }), ['table:unannotated-global']);
assert.deepEqual(rules({ 'a.cpp': 'std::vector<int> values(16);' }), ['values:unannotated-global']);
});
test('annotations live on definitions and source units include the owner header', () => {
const repository = { 'owner.cpp': 'int g_other;' };
assert.deepEqual(rules({ 'a.h': '// GLOBAL: gl_00005\nextern int g_other;' }, { repository }), ['g_other:annotated-extern']);
assert.deepEqual(rules({ 'a.cpp': 'extern int g_other;' }, { repository }), ['g_other:extern-in-unit']);
assert.deepEqual(rules({ 'a.cpp': '// GLOBAL: gl_00005\nextern int g_other = 1;' }), [], 'extern with an initializer really is a definition');
assert.deepEqual(rules({ 'a.h': 'extern __declspec(dllimport) int importedValue;' }), [], 'DLL imports intentionally have no project definition');
});
test('changed annotations and removed definitions are checked; unrelated legacy code is unchanged', () => {
const base = { 'a.h': 'extern int g_value;', 'a.cpp': '// GLOBAL: gl_00006\nint g_value;' };
const opts = { before: (f) => base[f] ?? null, repository: base };
assert.deepEqual(rules({ 'a.cpp': '' }, opts), ['g_value:undefined-extern']);
assert.deepEqual(rules({ 'a.cpp': 'int g_value;' }, opts), ['g_value:unannotated-global']);
assert.deepEqual(rules({ 'a.h': '// GLOBAL: gl_00006\nextern int g_value;' }, opts), ['g_value:annotated-extern']);
assert.deepEqual(rules({ 'a.cpp': '', 'owner.cpp': base['a.cpp'] }, opts), [], 'moving a definition does not orphan its extern');
assert.deepEqual(rules({ 'a.cpp': 'int legacy;\nvoid f() {}' }, { before: () => 'int legacy;' }), []);
});
test('comments, strings, prototypes and ordinary fields do not become globals', () => {
const text = '// extern int fake;\nvoid f(int a);\nstruct C {\n int member;\n void f();\n};\nvoid g() { int x; const char* s = "extern int fake;"; }';
assert.deepEqual(scanVariables(text), []);
assert.deepEqual(rules({ 'a.cpp': '// STRING: st_00001\nconst char* message = "x";' }), []);
assert.deepEqual(rules({ 'a.cpp': 'constexpr int table[2] = {1, 2};' }), ['table:unannotated-global'], 'constexpr arrays still occupy storage');
});
test('forward declarations, defaulted special members and default arguments are not variables', () => {
const text = 'class LuaScript; struct World; union Storage;\nC::C() = default; C::~C() = default;\nvoid f(int x = 4);\nint C::operator()(int x) { return x; }\n';
assert.deepEqual(scanVariables(text), []);
assert.deepEqual(rules({ 'a.cpp': 'void f() { if (true) { static int visits; } }' }), ['f::visits:unannotated-global']);
});
test('annotation edits cannot be hidden by unchanged declaration text or whitespace', () => {
const original = '// GLOBAL: gl_00001\nint g_count;';
assert.deepEqual(rules({ 'a.cpp': 'int g_count;' }, { before: () => original }), ['g_count:unannotated-global']);
assert.deepEqual(rules({ 'a.cpp': '// GLOBAL: gl_00001\n\n int\n g_count;' }, { before: () => original }), []);
});

View file

@ -1803,3 +1803,47 @@ test('an admin sets a reviewer weight: it counts at once, also on verdicts alrea
ctx.store.run(`UPDATE attempts SET state = 'superseded' WHERE id IN (?, ?, ?)`, waiting, alone, objected);
ctx.store.run('UPDATE tasks SET state = ? WHERE id = ?', saved.state, saved.id);
});
test('global rules reject undefined data in code and maintenance batches, including known symbol names', async () => {
const author = await activeUser('globalguard');
const maintainer = await activeUser('globalmaint', 'maintainer');
const unit = 'src/globalguard.cpp';
const oldRule = ctx.project.globalRules;
ctx.project.globalRules = true;
const v = ctx.store.currentVersion();
const fid = ctx.store.ensureSymbol({ kind: 'function', versionId: v.id, addr: 0x402A80, size: 8, unit }).symbol.id;
ctx.store.ensureSymbol({ kind: 'global', versionId: v.id, addr: 0x409200, name: 'g_guardMissing' });
ctx.tasks.generate();
const claim = await call('POST', '/api/regions/claim', { kind: 'unit', key: unit }, author);
assert.equal(claim.status, 200, JSON.stringify(claim.body));
const taskId = claim.body.functions.find((f) => f.symbol === fid).taskId;
const originalHead = ctx.repo.head();
const payload = { [unit]: `// FUNCTION: ${fid}\nint guardFunction() { return 1; }\n`, 'include/globalguard.h': 'extern int g_guardMissing;\n' };
const settle = (id, token) => waitFor(async () => {
const r = await call('GET', `/api/batches/${id}`, null, token);
return !['queued', 'verifying'].includes(r.body.state) && r.body;
});
try {
const submitted = await call('POST', '/api/batches', { taskIds: [taskId], baseCommit: originalHead, files: payload }, author);
assert.equal(submitted.status, 200, JSON.stringify(submitted.body));
assert.equal((await settle(submitted.body.batchId, author)).state, 'returned');
const feedback = JSON.parse(ctx.store.get('SELECT result_json FROM attempts WHERE id = ?', submitted.body.attempts[0].attemptId).result_json).feedback;
assert.deepEqual(feedback.globalErrors.map((e) => [e.name, e.rule]), [['g_guardMissing', 'undefined-extern']]);
const maintenance = await call('POST', '/api/maintenance', { baseCommit: originalHead, files: { 'include/globalguard.h': payload['include/globalguard.h'] } }, maintainer);
assert.equal(maintenance.status, 200);
const rejected = await settle(maintenance.body.batchId, maintainer);
assert.equal(rejected.state, 'returned');
assert.deepEqual(rejected.result.globalErrors.map((e) => e.rule), ['undefined-extern']);
assert.equal(ctx.repo.head(), originalHead);
assert.equal(ctx.repo.show('HEAD', 'include/globalguard.h'), null);
ctx.repo.commitFiles({ 'src/guardOwner.cpp': '// GLOBAL: gl_00001\nint g_guardMissing;\n' }, 'seed global owner', 'x <x@y.invalid>');
assert.deepEqual(ctx.verifier.globalsGate({ 'include/globalguard.h': payload['include/globalguard.h'] }, ctx.repo.head()), []);
ctx.repo.commitFiles({ 'include/globalguard.h': payload['include/globalguard.h'] }, 'seed declaration', 'x <x@y.invalid>');
assert.deepEqual(ctx.verifier.globalsGate({ 'src/guardOwner.cpp': '' }, ctx.repo.head()).map((e) => e.rule), ['undefined-extern']);
} finally {
ctx.project.globalRules = oldRule;
ctx.store.run(`UPDATE tasks SET state = 'done' WHERE id = ?`, taskId);
ctx.regions.release(ctx.accounts.byName('globalguard'), { kind: 'unit', key: unit });
}
});