parse adjacent method comments to populate description

This commit is contained in:
Dario Gabriel Lipicar
2026-06-03 15:13:54 -03:00
parent c71089abe9
commit 8b32e19fc5
10 changed files with 187 additions and 15 deletions
+15
View File
@@ -227,6 +227,21 @@ protected:
};
```
**Documenting methods:** a doc comment (`///` or `/** … */`) directly above a
method declaration becomes that method's `description` in the generated
`getMethods()` output, so it surfaces in `lm methods`, `logoscore module-info`,
and Basecamp's Methods list — no `describe` call needed:
```cpp
/// Processes the input and returns a result map.
LogosMap doWork(const std::string& input);
```
Plain `//` and `/* … */` comments are ignored (so section separators don't leak
into the API). The same applies to `interface: "provider"` modules whose methods
are marked with `LOGOS_METHOD`. See `cpp-generator/docs/spec.md`
*Method documentation* for details.
Available getters:
| Getter | Description |
+1 -1
View File
@@ -35,7 +35,7 @@ Shared data model used by all pipelines:
- **`TypeExpr`** — type expression with `Kind` (Primitive, Array, Map, Optional, Named), `name`, and `elements`
- **`ParamDecl`** — parameter name + type
- **`MethodDecl`** — method name, params, return type, `jsonReturn` flag (true when impl returns `LogosMap`/`LogosList`)
- **`MethodDecl`** — method name, params, return type, `description` (doc comment above the declaration, emitted into `getMethods()`), `jsonReturn` flag (true when impl returns `LogosMap`/`LogosList`)
- **`EventDecl`** — event name + params
- **`FieldDecl`** — struct field name, type, optional flag
- **`TypeDecl`** — named struct type with fields
+38 -2
View File
@@ -138,10 +138,46 @@ The `--from-header` mode parses a C++implementation header to extract public met
`LogosMap` and `LogosList` are `using` aliases for `nlohmann::json` defined in `logos_json.h` (part of the SDK). They allow module implementations to remain completely Qt-free while returning rich structured data. The parser maps them to the same LIDL shapes as `QVariantMap`/`QVariantList`, but sets the `jsonReturn` flag on the method so the generator emits an `nlohmannToQVariant()` conversion in the glue layer.
The parser uses a state machine to find the target class, track access specifiers (`public`/`private`/`protected`), and extract method declarations. It skips constructors, destructors, typedefs, using declarations, `std::function` members, and non-method statements.
The parser uses a state machine to find the target class, track access specifiers (`public`/`private`/`protected`), and extract method declarations. It skips constructors, destructors, typedefs, using declarations, `std::function` members, and non-method statements. While scanning, it also captures any doc comment immediately above a method declaration as that method's `description` (see [Method documentation](#method-documentation)).
Module metadata (name, version, description, dependencies) comes from `metadata.json`, not from the header.
### Method documentation
A doc comment written directly above a method's declaration in the impl header
becomes that method's `description`, stored on `MethodDecl.description` in the
shared AST and emitted into the `description` field of each `getMethods()`
entry. Because `getMethods()` is what the framework's `getPluginMethods()`
returns, the description flows — with no extra call — to `lm methods`,
`logoscore module-info`, and Basecamp's Methods list.
Only **doc comments** are captured: `///` line comments and `/** … */` /
`/*! … */` block comments. Plain `//` and `/* … */` comments are ignored, so
section separators and incidental notes don't leak into the API. Multi-line doc
comments are joined into a single-line description, and only comments
*immediately adjacent* to the declaration (no blank line in between) attach.
```cpp
class WalletModuleImpl : public LogosModuleContext {
public:
/// Transfers `amount` from the active account to `toAddress`.
/// Returns the resulting transaction hash.
std::string transfer(const std::string& toAddress, int64_t amount);
};
```
→ the `transfer` entry in `getMethods()` gains
`"description": "Transfers `amount` from the active account to `toAddress`. Returns the resulting transaction hash."`
The same applies to the legacy `--provider-header` mode (`LOGOS_METHOD`-marked
declarations): a doc comment above the declaration becomes the method's
`description` in the generated dispatch.
A method with no doc comment simply has no `description` field. Methods
introspected purely via Qt's `QMetaObject` (legacy `Q_INVOKABLE` modules with no
generated dispatch) carry no comments at runtime and therefore have no
`description`.
### Event Emission via `logos_events:`
Universal modules declare events in a Qt-`signals:`-style section parsed by the codegen. The same method name appears on both sides — declared in `logos_events:`, called directly to emit:
@@ -217,7 +253,7 @@ Contains two classes:
Implements two methods on the ProviderObject:
1. `**callMethod(methodName, args)`** — string-based dispatch table. For each method, extracts args from `QVariantList`, calls the typed wrapper, returns result as `QVariant`. Void methods return `QVariant(true)`.
2. `**getMethods()**` — returns `QJsonArray` of method metadata. Each entry has `name`, `signature`, `returnType`, `isInvokable`, and `parameters[]` (with `type` and `name`).
2. `**getMethods()**` — returns `QJsonArray` of method metadata. Each entry has `name`, `signature`, `returnType`, `isInvokable`, and `parameters[]` (with `type` and `name`). When the method's declaration in the impl header is preceded by a doc comment, the entry also carries a `description` (see [Method documentation](#method-documentation) below). This array is what the framework's `getPluginMethods()` returns, so the `description` surfaces in `lm methods`, `logoscore module-info`, and Basecamp's Methods list.
#### Client Stubs (`<name>_api.h` + `<name>_api.cpp`)
@@ -257,6 +257,11 @@ ImplParseResult parseImplHeader(const QString& headerPath,
State state = LookingForClass;
int braceDepth = 0;
// Accumulates doc-comment lines adjacent to a method so the doc comment
// becomes the method's description. Reset on any blank / non-comment line.
QStringList pendingDoc;
bool inBlockComment = false;
QRegularExpression classRe("\\bclass\\s+" + QRegularExpression::escape(className) + "\\b");
QRegularExpression accessRe("^\\s*(public|private|protected)\\s*:");
QRegularExpression eventsRe("^\\s*logos_events\\s*:");
@@ -280,6 +285,18 @@ ImplParseResult parseImplHeader(const QString& headerPath,
case InPublic:
case InPrivate:
case InLogosEvents:
// Inside a multi-line /** ... */ doc-comment block: capture its
// text (skip brace counting — comments don't affect scope).
if (inBlockComment) {
QString t = line;
int end = t.indexOf("*/");
if (end >= 0) { t = t.left(end); inBlockComment = false; }
t.remove(QRegularExpression(R"(^\*+\s?)"));
t = t.trimmed();
if (!t.isEmpty()) pendingDoc.append(t);
break;
}
for (QChar c : line) {
if (c == '{') braceDepth++;
else if (c == '}') braceDepth--;
@@ -297,6 +314,7 @@ ImplParseResult parseImplHeader(const QString& headerPath,
// token we recognise here.)
if (eventsRe.match(line).hasMatch()) {
state = InLogosEvents;
pendingDoc.clear();
break;
}
@@ -306,22 +324,51 @@ ImplParseResult parseImplHeader(const QString& headerPath,
QString spec = am.captured(1);
if (spec == "public") state = InPublic;
else state = InPrivate;
pendingDoc.clear();
break;
}
}
// Skip noise & non-declarations in any section.
if (line.isEmpty() || line.startsWith("//") || line.startsWith("#")
|| line.startsWith("/*") || line.startsWith("*"))
// Only doc comments (/// or /** ... */ / /*! ... */) accumulate as
// the pending description for the next method. Plain // and /*
// comments are ignored but leave pending doc intact; blank /
// preprocessor lines reset it so only *adjacent* comments attach.
if (line.startsWith("///")) {
QString text = line.mid(3);
if (text.startsWith('<')) text = text.mid(1); // ///< trailing form
text = text.trimmed();
if (!text.isEmpty()) pendingDoc.append(text);
break;
}
if (line.startsWith("/**") || line.startsWith("/*!")) {
QString text = line.mid(3);
int end = text.indexOf("*/");
if (end >= 0) text = text.left(end);
else inBlockComment = true;
text.remove(QRegularExpression(R"(^\*+\s?)"));
text = text.trimmed();
if (!text.isEmpty()) pendingDoc.append(text);
break;
}
if (line.startsWith("//") || line.startsWith("/*") || line.startsWith("*")) {
break; // non-doc comment: ignore, keep pending doc
}
if (line.isEmpty() || line.startsWith("#")) {
pendingDoc.clear();
break;
}
if (ctorDtorRe.match(line).hasMatch())
if (ctorDtorRe.match(line).hasMatch()) {
pendingDoc.clear();
break;
}
if (line.startsWith("typedef") || line.startsWith("using")
|| line.startsWith("friend") || line.startsWith("enum")
|| line.startsWith("struct"))
|| line.startsWith("struct")) {
pendingDoc.clear();
break;
}
if (state == InLogosEvents) {
// Inside `logos_events:` — every bare prototype is an event.
@@ -338,10 +385,11 @@ ImplParseResult parseImplHeader(const QString& headerPath,
result.module.events.append(ed);
}
}
pendingDoc.clear();
break;
}
if (state != InPublic) break;
if (state != InPublic) { pendingDoc.clear(); break; }
if (line.contains("std::function<")) {
// A std::function member is not a method — skip it so the
@@ -349,6 +397,7 @@ ImplParseResult parseImplHeader(const QString& headerPath,
// parens in its type. (Events are declared in a typed
// `logos_events:` section, parsed above — there is no longer
// any special `std::function emitEvent` member to detect.)
pendingDoc.clear();
break;
}
@@ -356,9 +405,11 @@ ImplParseResult parseImplHeader(const QString& headerPath,
QString decl = line.left(line.size() - 1).trimmed();
MethodDecl md;
if (parseMethodLine(decl, md)) {
md.description = pendingDoc.join(' ').trimmed();
result.module.methods.append(md);
}
}
pendingDoc.clear();
break;
}
}
+3
View File
@@ -40,6 +40,8 @@ struct MethodDecl {
QString name;
QVector<ParamDecl> params;
TypeExpr returnType;
// Doc comment adjacent to the method declaration (becomes "description").
QString description;
// True when the impl returns LogosMap or LogosList (nlohmann::json).
// The generator will emit nlohmann→Qt conversion code in the glue layer.
bool jsonReturn = false;
@@ -49,6 +51,7 @@ struct MethodDecl {
bool operator==(const MethodDecl& o) const {
return name == o.name && params == o.params && returnType == o.returnType
&& description == o.description
&& jsonReturn == o.jsonReturn && resultReturn == o.resultReturn;
}
};
@@ -489,6 +489,12 @@ QString lidlMakeProviderDispatch(const ModuleDecl& module)
s << " obj[\"name\"] = QStringLiteral(\"" << md.name << "\");\n";
s << " obj[\"returnType\"] = QStringLiteral(\"" << qtRet << "\");\n";
s << " obj[\"isInvokable\"] = true;\n";
if (!md.description.isEmpty()) {
QString escDesc = md.description;
escDesc.replace('\\', "\\\\");
escDesc.replace('"', "\\\"");
s << " obj[\"description\"] = QStringLiteral(\"" << escDesc << "\");\n";
}
QString sig = md.name + "(";
for (int i = 0; i < md.params.size(); ++i) {
+51 -3
View File
@@ -695,14 +695,62 @@ QVector<ParsedMethod> parseProviderHeader(const QString& headerPath, QTextStream
R"(^\s*LOGOS_METHOD\s+(.+?)\s+(\w+)\s*\(([^)]*)\)\s*;)"
);
// Accumulate comment lines immediately preceding a LOGOS_METHOD so the
// doc comment becomes the method's description. Reset on any blank or
// non-comment line, so only comments *adjacent* to the declaration count.
QStringList pendingDoc;
bool inBlockComment = false;
while (!in.atEnd()) {
QString line = in.readLine();
auto match = re.match(line);
if (!match.hasMatch()) continue;
QString rawLine = in.readLine();
QString line = rawLine.trimmed();
// Inside a multi-line /* ... */ block comment.
if (inBlockComment) {
QString text = line;
int end = text.indexOf("*/");
if (end >= 0) {
text = text.left(end);
inBlockComment = false;
}
text.remove(QRegularExpression(R"(^\*+\s?)")); // strip leading '*'
text = text.trimmed();
if (!text.isEmpty()) pendingDoc.append(text);
continue;
}
auto match = re.match(rawLine);
if (!match.hasMatch()) {
// Only doc comments (/// or /** ... */ / /*! ... */) become the
// description. Plain // and /* comments are ignored but leave any
// pending doc intact; blank / code lines reset it so only comments
// *adjacent* to the declaration attach.
if (line.startsWith("///")) {
QString text = line.mid(3);
if (text.startsWith('<')) text = text.mid(1); // ///< trailing form
text = text.trimmed();
if (!text.isEmpty()) pendingDoc.append(text);
} else if (line.startsWith("/**") || line.startsWith("/*!")) {
QString text = line.mid(3);
int end = text.indexOf("*/");
if (end >= 0) text = text.left(end);
else inBlockComment = true;
text.remove(QRegularExpression(R"(^\*+\s?)"));
text = text.trimmed();
if (!text.isEmpty()) pendingDoc.append(text);
} else if (line.startsWith("//") || line.startsWith("/*") || line.startsWith("*")) {
// Non-doc comment: ignore, keep any pending doc comment.
} else {
pendingDoc.clear();
}
continue;
}
ParsedMethod m;
m.returnType = normalizeType(match.captured(1));
m.name = match.captured(2);
m.description = pendingDoc.join(' ').trimmed();
pendingDoc.clear();
QString paramStr = match.captured(3).trimmed();
if (!paramStr.isEmpty()) {
+1
View File
@@ -11,6 +11,7 @@ struct ParsedMethod {
QString returnType;
QString name;
QVector<QPair<QString, QString>> params; // (type, name)
QString description; // doc comment adjacent to the LOGOS_METHOD declaration
};
// Which type surface to expose on the generated per-module wrapper.
+12
View File
@@ -18,6 +18,15 @@
#include "generator_lib.h"
#include "../experimental/lidl_parser.h"
// Escape a string for safe embedding inside a generated C++ string literal.
static QString cppStringEscape(const QString& s)
{
QString out = s;
out.replace('\\', "\\\\");
out.replace('"', "\\\"");
return out;
}
// Convert a TypeExpr → Qt-typed string name (same surface the
// metaobject-introspection path produces for methods, so generator_lib
// can consume both via one code path).
@@ -411,6 +420,9 @@ static int generateProviderDispatch(const QString& headerPath, const QString& ou
s << " obj[\"name\"] = QStringLiteral(\"" << m.name << "\");\n";
s << " obj[\"returnType\"] = QStringLiteral(\"" << m.returnType << "\");\n";
s << " obj[\"isInvokable\"] = true;\n";
if (!m.description.isEmpty()) {
s << " obj[\"description\"] = QStringLiteral(\"" << cppStringEscape(m.description) << "\");\n";
}
QString sig = m.name + "(";
for (int i = 0; i < m.params.size(); ++i) {
sig += m.params[i].first;
+3 -3
View File
@@ -201,7 +201,7 @@ Modules never instantiate `ModuleProxy` directly; it is created by the provider
**Responsibilities**:
- Validate the authentication token on every remote call. In `callRemoteMethod()` the proxy checks that a nonempty token is provided and verifies it against the `TokenManager`. Calls with invalid or missing tokens return an empty `QVariant`.
- Dispatch method calls to the underlying module using Qts metaobject system. The proxy locates the requested method by name and argument count, supports up to five arguments, and handles various return types including `void`, `bool`, `int`, `QString`, `QVariant`, `QJsonArray` and `QStringList`
- Introspect the wrapped modules API via `getPluginMethods()`, returning a `QJsonArray` describing each method (name, signature, return type, parameters)
- Introspect the wrapped modules API via `getPluginMethods()`, returning a `QJsonArray` describing each method (name, signature, return type, parameters, and — when the method has a doc comment in its header — a `description`)
- Provide an `eventResponse` signal that the provider emits when events are forwarded to subscribers
- Store tokens issued by other modules via `saveToken(fromModuleName, token)`
- Allow a module or consumer to inform another module of a token via `informModuleToken(authToken, moduleName, token)`
@@ -211,7 +211,7 @@ Modules never instantiate `ModuleProxy` directly; it is created by the provider
| `explicit ModuleProxy(QObject* module, QObject *parent = nullptr)` | Wraps `module` for remote access. |
| `QVariant callRemoteMethod(const QString& authToken, const QString& methodName, const QVariantList& args = {})` | Validates `authToken`, locates `methodName` on the module and invokes it. Supports up to five arguments and multiple return types. This will forward the request to the wrapped object. |
| `bool informModuleToken(const QString& authToken, const QString& moduleName, const QString& token)` | Stores `token` for `moduleName` in the global `TokenManager`. This is used by the core and capability module to let this module know that another module will communicate using a certain token,=. |
| `QJsonArray getPluginMethods()` | Enumerates the wrapped modules methods using Qt metaobject introspection and returns a JSON array with signatures and parameters. |
| `QJsonArray getPluginMethods()` | Enumerates the wrapped modules methods and returns a JSON array with signatures and parameters. Generated provider/universal modules also include a per-method `description` (from the method's header doc comment); legacy modules introspected via Qt metaobject have none. |
| `eventResponse(QString eventName, QVariantList data)` (signal) | Emitted when the proxy forwards an event to subscribers. |
Example: Listing methods of a module (from a consumer)
@@ -491,7 +491,7 @@ signals:
|--------|---------|
| `callRemoteMethod(authToken, methodName, args) → QVariant` | Validates `authToken`, locates `methodName` on the module and invokes it |
| `informModuleToken(authToken, moduleName, token) → bool` | Stores `token` for `moduleName` in the global `TokenManager` |
| `getPluginMethods() → QJsonArray` | Enumerates the wrapped module's methods using Qt meta-object introspection |
| `getPluginMethods() → QJsonArray` | Enumerates the wrapped module's methods (name, signature, return type, parameters, and a per-method `description` for documented provider/universal methods) |
**Responsibilities**:
- Enforce token validation on every inbound call (returns invalid `QVariant` on failure).