diff --git a/README.md b/README.md index 069bea4..1ee0177 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/cpp-generator/docs/project.md b/cpp-generator/docs/project.md index e160149..f27be05 100644 --- a/cpp-generator/docs/project.md +++ b/cpp-generator/docs/project.md @@ -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 diff --git a/cpp-generator/docs/spec.md b/cpp-generator/docs/spec.md index e1c7f36..2484f83 100644 --- a/cpp-generator/docs/spec.md +++ b/cpp-generator/docs/spec.md @@ -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 (`_api.h` + `_api.cpp`) diff --git a/cpp-generator/experimental/impl_header_parser.cpp b/cpp-generator/experimental/impl_header_parser.cpp index a769869..df43d15 100644 --- a/cpp-generator/experimental/impl_header_parser.cpp +++ b/cpp-generator/experimental/impl_header_parser.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; } } diff --git a/cpp-generator/experimental/lidl_ast.h b/cpp-generator/experimental/lidl_ast.h index 136f20f..d4d05ef 100644 --- a/cpp-generator/experimental/lidl_ast.h +++ b/cpp-generator/experimental/lidl_ast.h @@ -40,6 +40,8 @@ struct MethodDecl { QString name; QVector 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; } }; diff --git a/cpp-generator/experimental/lidl_gen_provider.cpp b/cpp-generator/experimental/lidl_gen_provider.cpp index c3bbe68..ea80062 100644 --- a/cpp-generator/experimental/lidl_gen_provider.cpp +++ b/cpp-generator/experimental/lidl_gen_provider.cpp @@ -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) { diff --git a/cpp-generator/legacy/generator_lib.cpp b/cpp-generator/legacy/generator_lib.cpp index 641fdba..cd9483b 100644 --- a/cpp-generator/legacy/generator_lib.cpp +++ b/cpp-generator/legacy/generator_lib.cpp @@ -695,14 +695,62 @@ QVector 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()) { diff --git a/cpp-generator/legacy/generator_lib.h b/cpp-generator/legacy/generator_lib.h index 89e8279..58b2bef 100644 --- a/cpp-generator/legacy/generator_lib.h +++ b/cpp-generator/legacy/generator_lib.h @@ -11,6 +11,7 @@ struct ParsedMethod { QString returnType; QString name; QVector> params; // (type, name) + QString description; // doc comment adjacent to the LOGOS_METHOD declaration }; // Which type surface to expose on the generated per-module wrapper. diff --git a/cpp-generator/legacy/main.cpp b/cpp-generator/legacy/main.cpp index 7e1808b..b1b00c3 100644 --- a/cpp-generator/legacy/main.cpp +++ b/cpp-generator/legacy/main.cpp @@ -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; diff --git a/docs/docs.md b/docs/docs.md index 2c6efa4..36b5c56 100644 --- a/docs/docs.md +++ b/docs/docs.md @@ -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 non‑empty 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 Qt’s meta‑object 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 module’s API via `getPluginMethods()`, returning a `QJsonArray` describing each method (name, signature, return type, parameters) +- Introspect the wrapped module’s 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 module’s methods using Qt meta‑object introspection and returns a JSON array with signatures and parameters. | +| `QJsonArray getPluginMethods()` | Enumerates the wrapped module’s 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 meta‑object 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).