Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

## 0.3.15 - 2026-06-12

### Fixed

- Detect deprecated `loadSessionStore(...)` usage when plugin code calls it through a runtime session API alias.

## 0.3.14 - 2026-06-11

### Changed
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openclaw/plugin-inspector",
"version": "0.3.14",
"version": "0.3.15",
"private": false,
"description": "Offline compatibility inspector for OpenClaw plugins.",
"type": "module",
Expand Down
167 changes: 167 additions & 0 deletions src/sdk-deprecation-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ function collectLoadSessionStoreDeprecations(findings, context) {
collectNamespaceUsageDeprecations(findings, context);
collectNamespaceRequireDeprecations(findings, context);
collectRuntimeUsageDeprecations(findings, context);
collectRuntimeAliasUsageDeprecations(findings, context);
}

function collectNamedImportDeprecations(findings, context) {
Expand Down Expand Up @@ -298,6 +299,172 @@ function collectRuntimeUsageDeprecations(findings, context) {
});
}

function collectRuntimeAliasUsageDeprecations(findings, context) {
const aliases = collectRuntimeSessionAliases(context.text);
if (aliases.size === 0) {
return;
}
collectMemberCallDeprecations(findings, context, {
receiverMatcher: (receiver) => aliases.has(receiver),
surface: "api.runtime.agent.session alias",
});
}

function collectRuntimeSessionAliases(text) {
const aliases = new Set();
const factories = collectRuntimeSessionFactoryNames(text);
collectDirectRuntimeSessionAliases(text, aliases);
collectFactoryCallRuntimeSessionAliases(text, aliases, factories);
return aliases;
}

function collectRuntimeSessionFactoryNames(text) {
const factories = new Set();
for (const fn of findNamedFunctionBodies(text)) {
const aliases = new Set();
collectDirectRuntimeSessionAliases(fn.body, aliases);
if (aliases.size === 0) {
continue;
}
const returnRegex = /\breturn\s+([A-Za-z_$][\w$]*)\b/g;
for (const match of fn.body.matchAll(returnRegex)) {
if (aliases.has(match[1])) {
factories.add(fn.name);
}
}
}
return factories;
}

function findNamedFunctionBodies(text) {
const functions = [];
let searchStart = 0;
while (searchStart < text.length) {
const keywordOffset = text.indexOf("function", searchStart);
if (keywordOffset === -1) {
break;
}
searchStart = keywordOffset + "function".length;
if (!isIdentifierBoundary(text, keywordOffset - 1) || !isIdentifierBoundary(text, searchStart)) {
continue;
}
let cursor = skipWhitespaceForward(text, searchStart);
const name = readIdentifierAt(text, cursor);
if (!name) {
continue;
}
cursor = skipWhitespaceForward(text, name.end);
if (text[cursor] !== "(") {
continue;
}
const paramsEnd = findMatchingDelimiter(text, cursor, "(", ")");
if (paramsEnd === -1) {
continue;
}
cursor = skipWhitespaceForward(text, paramsEnd + 1);
while (cursor < text.length && text[cursor] !== "{") {
cursor += 1;
}
if (text[cursor] !== "{") {
break;
}
const bodyStart = cursor + 1;
const bodyEnd = findMatchingBrace(text, cursor);
if (bodyEnd === -1) {
continue;
}
functions.push({
name: name.value,
body: text.slice(bodyStart, bodyEnd),
});
searchStart = bodyEnd + 1;
}
return functions;
}

function readIdentifierAt(text, startOffset) {
const first = text[startOffset];
if (!/[A-Za-z_$]/.test(first)) {
return null;
}
let end = startOffset + 1;
while (end < text.length && /[A-Za-z0-9_$]/.test(text[end])) {
end += 1;
}
return {
value: text.slice(startOffset, end),
end,
};
}

function findMatchingDelimiter(text, openOffset, openChar, closeChar) {
let depth = 0;
for (let index = openOffset; index < text.length; index += 1) {
const char = text[index];
if (char === openChar) {
depth += 1;
} else if (char === closeChar) {
depth -= 1;
if (depth === 0) {
return index;
}
}
}
return -1;
}

function findMatchingBrace(text, openOffset) {
return findMatchingDelimiter(text, openOffset, "{", "}");
}

function collectDirectRuntimeSessionAliases(text, aliases) {
const runtimeAliases = collectRuntimeObjectAliases(text);
const declarationRegex = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([^;\n]+)/g;
for (const match of text.matchAll(declarationRegex)) {
const local = match[1];
const initializer = normalizeReceiverExpression(match[2]);
if (isRuntimeSessionExpression(initializer, runtimeAliases)) {
aliases.add(local);
}
}
}

function collectRuntimeObjectAliases(text) {
const aliases = new Set();
const declarationRegex = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:\([^)]*\)\s*)?(?:[A-Za-z_$][\w$]*|this)\.runtime\b/g;
for (const match of text.matchAll(declarationRegex)) {
aliases.add(match[1]);
}
return aliases;
}

function isRuntimeSessionExpression(expression, runtimeAliases) {
for (const part of expression.split("??")) {
const candidate = part.trim();
if (isRuntimeSessionReceiver(candidate)) {
return true;
}
for (const alias of runtimeAliases) {
if (candidate === `${alias}.agent.session` || candidate === `${alias}.channel.session`) {
return true;
}
}
}
return false;
}

function collectFactoryCallRuntimeSessionAliases(text, aliases, factories) {
if (factories.size === 0) {
return;
}
const declarationRegex = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\s*\(/g;
for (const match of text.matchAll(declarationRegex)) {
if (factories.has(match[2])) {
aliases.add(match[1]);
}
}
}

function parseNamedBindings(rawBindings, options = {}) {
const aliasSeparator = options.aliasSeparator ?? "as";
return rawBindings
Expand Down
27 changes: 27 additions & 0 deletions test/inspector.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,33 @@ test("source inspection records optional-chained whole-store session helper usag
);
});

test("source inspection records whole-store session helper usage through runtime session aliases", () => {
const inspection = inspectSourceText(
[
"function getRuntimeAgentSessionApi(api) {",
" const runtime = api.runtime;",
" const runtimeSessionApi = runtime.agent?.session ?? runtime.channel?.session;",
" return runtimeSessionApi;",
"}",
"",
"export function register(api) {",
" const sessionApi = getRuntimeAgentSessionApi(api);",
" if (!sessionApi) {",
" return;",
" }",
" const store = sessionApi.loadSessionStore('/tmp/sessions.json');",
" return store;",
"}",
].join("\n"),
"plugins/example/index.ts",
);

assert.deepEqual(
inspection.sdkDeprecations.map((finding) => `${finding.surface}@${finding.ref}`),
["api.runtime.agent.session alias@plugins/example/index.ts:12"],
);
});

test("fixture set inspection produces a passing report", async () => {
const config = await loadInspectorConfig("test/fixtures/inspector.config.json");
const report = await inspectFixtureSet(config);
Expand Down
Loading