c-next 0.1.60 → 0.1.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/grammar/CNext.g4 +3 -17
- package/package.json +1 -1
- package/src/cli/serve/ServeCommand.ts +166 -35
- package/src/cli/serve/__tests__/ServeCommand.test.ts +227 -3
- package/src/lib/__tests__/parseCHeader.mocked.test.ts +145 -0
- package/src/lib/__tests__/parseCHeader.test.ts +194 -0
- package/src/lib/parseCHeader.ts +114 -0
- package/src/lib/types/TSymbolKind.ts +2 -1
- package/src/transpiler/Transpiler.ts +676 -618
- package/src/transpiler/__tests__/DualCodePaths.test.ts +4 -0
- package/src/transpiler/__tests__/Transpiler.coverage.test.ts +1 -98
- package/src/transpiler/__tests__/Transpiler.test.ts +3 -3
- package/src/transpiler/data/IncludeTreeWalker.ts +1 -1
- package/src/transpiler/logic/analysis/FunctionCallAnalyzer.ts +74 -46
- package/src/transpiler/logic/analysis/InitializationAnalyzer.ts +126 -120
- package/src/transpiler/logic/analysis/NullCheckAnalyzer.ts +44 -35
- package/src/transpiler/logic/parser/grammar/CNext.interp +1 -3
- package/src/transpiler/logic/parser/grammar/CNextListener.ts +0 -22
- package/src/transpiler/logic/parser/grammar/CNextParser.ts +665 -1084
- package/src/transpiler/logic/parser/grammar/CNextVisitor.ts +0 -14
- package/src/transpiler/logic/symbols/CSymbolCollector.ts +23 -16
- package/src/transpiler/logic/symbols/CppSymbolCollector.ts +67 -43
- package/src/transpiler/logic/symbols/cnext/index.ts +47 -27
- package/src/transpiler/output/codegen/CodeGenerator.ts +875 -1647
- package/src/transpiler/output/codegen/TypeResolver.ts +56 -32
- package/src/transpiler/output/codegen/TypeValidator.ts +24 -75
- package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +1764 -0
- package/src/transpiler/output/codegen/__tests__/ExpressionWalker.test.ts +10 -7
- package/src/transpiler/output/codegen/__tests__/TrackVariableTypeHelpers.test.ts +38 -37
- package/src/transpiler/output/codegen/analysis/MemberChainAnalyzer.ts +211 -46
- package/src/transpiler/output/codegen/analysis/StringLengthCounter.ts +3 -3
- package/src/transpiler/output/codegen/analysis/__tests__/MemberChainAnalyzer.test.ts +191 -238
- package/src/transpiler/output/codegen/assignment/AssignmentClassifier.ts +60 -15
- package/src/transpiler/output/codegen/assignment/AssignmentContextBuilder.ts +75 -75
- package/src/transpiler/output/codegen/assignment/AssignmentKind.ts +3 -0
- package/src/transpiler/output/codegen/assignment/IAssignmentContext.ts +3 -0
- package/src/transpiler/output/codegen/assignment/__tests__/AssignmentClassifier.test.ts +3 -0
- package/src/transpiler/output/codegen/assignment/handlers/BitAccessHandlers.ts +42 -0
- package/src/transpiler/output/codegen/assignment/handlers/__tests__/BitAccessHandlers.test.ts +76 -2
- package/src/transpiler/output/codegen/generators/IOrchestrator.ts +5 -1
- package/src/transpiler/output/codegen/generators/__tests__/GeneratorRegistry.test.ts +51 -0
- package/src/transpiler/output/codegen/generators/declarationGenerators/RegisterGenerator.ts +11 -24
- package/src/transpiler/output/codegen/generators/declarationGenerators/RegisterMacroGenerator.ts +64 -0
- package/src/transpiler/output/codegen/generators/declarationGenerators/ScopeGenerator.ts +16 -10
- package/src/transpiler/output/codegen/generators/declarationGenerators/ScopedRegisterGenerator.ts +18 -27
- package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +5 -1
- package/src/transpiler/output/codegen/generators/statements/ControlFlowGenerator.ts +1 -17
- package/src/transpiler/output/codegen/generators/statements/SwitchGenerator.ts +77 -40
- package/src/transpiler/output/codegen/generators/support/IncludeGenerator.ts +93 -47
- package/src/transpiler/output/codegen/helpers/ArrayAccessHelper.ts +129 -0
- package/src/transpiler/output/codegen/helpers/AssignmentExpectedTypeResolver.ts +23 -18
- package/src/transpiler/output/codegen/helpers/AssignmentTargetExtractor.ts +58 -0
- package/src/transpiler/output/codegen/helpers/AssignmentValidator.ts +33 -31
- package/src/transpiler/output/codegen/helpers/BaseIdentifierBuilder.ts +53 -0
- package/src/transpiler/output/codegen/helpers/CastValidator.ts +146 -0
- package/src/transpiler/output/codegen/helpers/ChildStatementCollector.ts +116 -0
- package/src/transpiler/output/codegen/helpers/LiteralEvaluator.ts +61 -0
- package/src/transpiler/output/codegen/helpers/MemberSeparatorResolver.ts +10 -3
- package/src/transpiler/output/codegen/helpers/PostfixChainBuilder.ts +101 -0
- package/src/transpiler/output/codegen/helpers/SimpleIdentifierResolver.ts +54 -0
- package/src/transpiler/output/codegen/helpers/StatementExpressionCollector.ts +117 -0
- package/src/transpiler/output/codegen/helpers/SymbolLookupHelper.ts +44 -0
- package/src/transpiler/output/codegen/helpers/TransitiveModificationPropagator.ts +127 -0
- package/src/transpiler/output/codegen/helpers/TypeGenerationHelper.ts +249 -0
- package/src/transpiler/output/codegen/helpers/__tests__/ArrayAccessHelper.test.ts +479 -0
- package/src/transpiler/output/codegen/helpers/__tests__/AssignmentTargetExtractor.test.ts +105 -0
- package/src/transpiler/output/codegen/helpers/__tests__/BaseIdentifierBuilder.test.ts +68 -0
- package/src/transpiler/output/codegen/helpers/__tests__/CastValidator.test.ts +233 -0
- package/src/transpiler/output/codegen/helpers/__tests__/ChildStatementCollector.test.ts +191 -0
- package/src/transpiler/output/codegen/helpers/__tests__/LiteralEvaluator.test.ts +62 -0
- package/src/transpiler/output/codegen/helpers/__tests__/MemberSeparatorResolver.test.ts +1 -0
- package/src/transpiler/output/codegen/helpers/__tests__/PostfixChainBuilder.test.ts +147 -0
- package/src/transpiler/output/codegen/helpers/__tests__/SimpleIdentifierResolver.test.ts +105 -0
- package/src/transpiler/output/codegen/helpers/__tests__/StatementExpressionCollector.test.ts +221 -0
- package/src/transpiler/output/codegen/helpers/__tests__/SymbolLookupHelper.test.ts +201 -0
- package/src/transpiler/output/codegen/helpers/__tests__/TransitiveModificationPropagator.test.ts +289 -0
- package/src/transpiler/output/codegen/helpers/__tests__/TypeGenerationHelper.test.ts +419 -0
- package/src/transpiler/output/codegen/types/IArrayAccessDeps.ts +23 -0
- package/src/transpiler/output/codegen/types/IArrayAccessInfo.ts +26 -0
- package/src/transpiler/output/codegen/types/IBaseIdentifierResult.ts +11 -0
- package/src/transpiler/output/codegen/types/IMemberSeparatorDeps.ts +7 -0
- package/src/transpiler/output/codegen/types/IPostfixChainDeps.ts +16 -0
- package/src/transpiler/output/codegen/types/IPostfixOperation.ts +11 -0
- package/src/transpiler/output/codegen/types/ISimpleIdentifierDeps.ts +21 -0
- package/src/transpiler/output/codegen/utils/CodegenParserUtils.ts +98 -0
- package/src/transpiler/output/codegen/utils/ExpressionUnwrapper.ts +22 -22
- package/src/transpiler/output/codegen/utils/__tests__/CodegenParserUtils.test.ts +228 -0
- package/src/transpiler/types/IFileResult.ts +0 -4
- package/src/transpiler/types/IPipelineFile.ts +27 -0
- package/src/transpiler/types/IPipelineInput.ts +23 -0
- package/src/transpiler/types/TranspilerState.ts +1 -1
- package/src/utils/FormatUtils.ts +28 -2
- package/src/utils/MapUtils.ts +25 -0
- package/src/utils/PostfixAnalysisUtils.ts +48 -0
- package/src/utils/__tests__/FormatUtils.test.ts +42 -0
- package/src/utils/__tests__/MapUtils.test.ts +85 -0
- package/src/utils/constants/OperatorMappings.ts +19 -0
- package/src/lib/__tests__/transpiler.test.ts +0 -303
- package/src/lib/transpiler.ts +0 -149
- package/src/lib/types/ITranspileOptions.ts +0 -15
- package/src/lib/types/ITranspileResult.ts +0 -20
- package/src/transpiler/logic/StandaloneContextBuilder.ts +0 -150
- package/src/transpiler/logic/__tests__/StandaloneContextBuilder.test.ts +0 -647
- package/src/transpiler/types/ITranspileContext.ts +0 -49
- package/src/transpiler/types/ITranspileContribution.ts +0 -32
package/README.md
CHANGED
|
@@ -94,6 +94,14 @@ cnext src/ -o build/src --header-out build/include --clean
|
|
|
94
94
|
cnext --help
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
## VS Code Extension
|
|
98
|
+
|
|
99
|
+
The C-Next VS Code extension provides syntax highlighting, live C preview, IntelliSense, and error diagnostics.
|
|
100
|
+
|
|
101
|
+
**Install from:** [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=jlaustill.vscode-c-next) (coming soon)
|
|
102
|
+
|
|
103
|
+
**Source:** [github.com/jlaustill/vscode-c-next](https://github.com/jlaustill/vscode-c-next)
|
|
104
|
+
|
|
97
105
|
## Getting Started with PlatformIO
|
|
98
106
|
|
|
99
107
|
C-Next integrates seamlessly with PlatformIO embedded projects. The transpiler automatically converts `.cnx` files to `.c` before each build.
|
package/grammar/CNext.g4
CHANGED
|
@@ -248,14 +248,11 @@ assignmentOperator
|
|
|
248
248
|
;
|
|
249
249
|
|
|
250
250
|
// Assignment target with unified postfix chain approach (Issue #387)
|
|
251
|
-
//
|
|
252
|
-
// Bare identifiers fall back to existing memberAccess/arrayAccess rules for compatibility
|
|
251
|
+
// All patterns use postfixTargetOp* for member/array access chains
|
|
253
252
|
assignmentTarget
|
|
254
253
|
: 'global' '.' IDENTIFIER postfixTargetOp* // global.x, global.x[i][j].y, etc.
|
|
255
254
|
| 'this' '.' IDENTIFIER postfixTargetOp* // this.x, this.x[i].y, etc.
|
|
256
|
-
|
|
|
257
|
-
| memberAccess // GPIO7.DR_SET, arr[i].field
|
|
258
|
-
| IDENTIFIER // Simple identifier
|
|
255
|
+
| IDENTIFIER postfixTargetOp* // x, arr[i], obj.field, arr[i].field, etc.
|
|
259
256
|
;
|
|
260
257
|
|
|
261
258
|
// Unified postfix operation for assignment targets (Issue #387)
|
|
@@ -459,18 +456,7 @@ arrayInitializerElement
|
|
|
459
456
|
| arrayInitializer // For nested arrays: [[1,2], [3,4]]
|
|
460
457
|
;
|
|
461
458
|
|
|
462
|
-
memberAccess
|
|
463
|
-
: IDENTIFIER ('.' IDENTIFIER)+ ('[' expression ']')+ // ADR-036: screen.pixels[0][0]
|
|
464
|
-
| IDENTIFIER ('.' IDENTIFIER)+ '[' expression ',' expression ']' // GPIO7.DR[start, width]
|
|
465
|
-
| IDENTIFIER ('.' IDENTIFIER)+ // GPIO7.DR_SET
|
|
466
|
-
| IDENTIFIER ('[' expression ']')+ ('.' IDENTIFIER)+ // arr[i].field1.field2...
|
|
467
|
-
| IDENTIFIER (('[' expression ']') | ('.' IDENTIFIER))+ // arr[i].field[j].member... (any mix)
|
|
468
|
-
;
|
|
469
|
-
|
|
470
|
-
arrayAccess
|
|
471
|
-
: IDENTIFIER '[' expression ']' // Single element/bit
|
|
472
|
-
| IDENTIFIER '[' expression ',' expression ']' // Bit range [start, width]
|
|
473
|
-
;
|
|
459
|
+
// Note: memberAccess and arrayAccess rules removed - unified into assignmentTarget with postfixTargetOp*
|
|
474
460
|
|
|
475
461
|
argumentList
|
|
476
462
|
: expression (',' expression)*
|
package/package.json
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ServeCommand
|
|
3
3
|
* JSON-RPC server for VS Code extension communication
|
|
4
|
+
*
|
|
5
|
+
* Phase 2b (ADR-060): Uses the full Transpiler for transpilation and symbol
|
|
6
|
+
* extraction, enabling include resolution, C++ auto-detection, and cross-file
|
|
7
|
+
* symbol support.
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
import { createInterface, Interface } from "node:readline";
|
|
11
|
+
import { dirname } from "node:path";
|
|
7
12
|
import JsonRpcHandler from "./JsonRpcHandler";
|
|
8
13
|
import IJsonRpcRequest from "./types/IJsonRpcRequest";
|
|
9
14
|
import IJsonRpcResponse from "./types/IJsonRpcResponse";
|
|
10
15
|
import ConfigPrinter from "../ConfigPrinter";
|
|
11
|
-
import
|
|
16
|
+
import ConfigLoader from "../ConfigLoader";
|
|
17
|
+
import Transpiler from "../../transpiler/Transpiler";
|
|
12
18
|
import parseWithSymbols from "../../lib/parseWithSymbols";
|
|
19
|
+
import parseCHeader from "../../lib/parseCHeader";
|
|
13
20
|
|
|
14
21
|
/**
|
|
15
|
-
* Method handler type
|
|
22
|
+
* Method handler type (async to support Transpiler.transpileSource)
|
|
16
23
|
*/
|
|
17
|
-
type MethodHandler = (
|
|
24
|
+
type MethodHandler = (
|
|
25
|
+
params?: Record<string, unknown>,
|
|
26
|
+
) => Promise<IMethodResult>;
|
|
18
27
|
|
|
19
28
|
/**
|
|
20
29
|
* Result from a method handler
|
|
@@ -26,6 +35,14 @@ interface IMethodResult {
|
|
|
26
35
|
errorMessage?: string;
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Result of validating and extracting source parameters.
|
|
40
|
+
*/
|
|
41
|
+
interface ISourceParams {
|
|
42
|
+
source: string;
|
|
43
|
+
filePath: string | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
29
46
|
/**
|
|
30
47
|
* Options for the serve command
|
|
31
48
|
*/
|
|
@@ -41,14 +58,23 @@ class ServeCommand {
|
|
|
41
58
|
private static shouldShutdown = false;
|
|
42
59
|
private static readline: Interface | null = null;
|
|
43
60
|
private static debugMode = false;
|
|
61
|
+
private static transpiler: Transpiler | null = null;
|
|
44
62
|
|
|
45
63
|
/**
|
|
46
64
|
* Method handlers registry
|
|
47
65
|
*/
|
|
48
66
|
private static readonly methods: Record<string, MethodHandler> = {
|
|
49
67
|
getVersion: ServeCommand.handleGetVersion,
|
|
50
|
-
|
|
51
|
-
|
|
68
|
+
initialize: ServeCommand.handleInitialize,
|
|
69
|
+
transpile: ServeCommand._withSourceValidation(
|
|
70
|
+
ServeCommand._handleTranspile,
|
|
71
|
+
),
|
|
72
|
+
parseSymbols: ServeCommand._withSourceValidation(
|
|
73
|
+
ServeCommand._handleParseSymbols,
|
|
74
|
+
),
|
|
75
|
+
parseCHeader: ServeCommand._withSourceValidation(
|
|
76
|
+
ServeCommand._handleParseCHeader,
|
|
77
|
+
),
|
|
52
78
|
shutdown: ServeCommand.handleShutdown,
|
|
53
79
|
};
|
|
54
80
|
|
|
@@ -92,6 +118,34 @@ class ServeCommand {
|
|
|
92
118
|
}
|
|
93
119
|
}
|
|
94
120
|
|
|
121
|
+
// ========================================================================
|
|
122
|
+
// Parameter Validation Helpers (Issue #707: Reduce code duplication)
|
|
123
|
+
// ========================================================================
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Wrapper that validates source params before calling handler.
|
|
127
|
+
* Eliminates duplicate validation code across handlers.
|
|
128
|
+
*/
|
|
129
|
+
private static _withSourceValidation(
|
|
130
|
+
handler: (params: ISourceParams) => Promise<IMethodResult>,
|
|
131
|
+
): MethodHandler {
|
|
132
|
+
return async (params?: Record<string, unknown>): Promise<IMethodResult> => {
|
|
133
|
+
if (!params || typeof params.source !== "string") {
|
|
134
|
+
return {
|
|
135
|
+
success: false,
|
|
136
|
+
errorCode: JsonRpcHandler.ERROR_INVALID_PARAMS,
|
|
137
|
+
errorMessage: "Missing required param: source",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const validated: ISourceParams = {
|
|
141
|
+
source: String(params.source),
|
|
142
|
+
filePath:
|
|
143
|
+
typeof params.filePath === "string" ? params.filePath : undefined,
|
|
144
|
+
};
|
|
145
|
+
return handler(validated);
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
95
149
|
/**
|
|
96
150
|
* Handle a single line of input
|
|
97
151
|
*/
|
|
@@ -115,20 +169,23 @@ class ServeCommand {
|
|
|
115
169
|
const request = parseResult.request!;
|
|
116
170
|
this.log(`method: ${request.method}`);
|
|
117
171
|
|
|
118
|
-
// Dispatch to method handler
|
|
119
|
-
|
|
120
|
-
|
|
172
|
+
// Dispatch to method handler (async)
|
|
173
|
+
this.dispatch(request).then((response) => {
|
|
174
|
+
this.writeResponse(response);
|
|
121
175
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
176
|
+
// Handle shutdown after response is written
|
|
177
|
+
if (this.shouldShutdown) {
|
|
178
|
+
this.readline?.close();
|
|
179
|
+
}
|
|
180
|
+
});
|
|
126
181
|
}
|
|
127
182
|
|
|
128
183
|
/**
|
|
129
184
|
* Dispatch a request to the appropriate handler
|
|
130
185
|
*/
|
|
131
|
-
private static dispatch(
|
|
186
|
+
private static async dispatch(
|
|
187
|
+
request: IJsonRpcRequest,
|
|
188
|
+
): Promise<IJsonRpcResponse> {
|
|
132
189
|
const handler = this.methods[request.method];
|
|
133
190
|
|
|
134
191
|
if (!handler) {
|
|
@@ -139,7 +196,7 @@ class ServeCommand {
|
|
|
139
196
|
);
|
|
140
197
|
}
|
|
141
198
|
|
|
142
|
-
const result = handler(request.params);
|
|
199
|
+
const result = await handler(request.params);
|
|
143
200
|
|
|
144
201
|
if (result.success) {
|
|
145
202
|
return JsonRpcHandler.formatResponse(request.id, result.result);
|
|
@@ -162,7 +219,7 @@ class ServeCommand {
|
|
|
162
219
|
/**
|
|
163
220
|
* Handle getVersion method
|
|
164
221
|
*/
|
|
165
|
-
private static handleGetVersion(): IMethodResult {
|
|
222
|
+
private static async handleGetVersion(): Promise<IMethodResult> {
|
|
166
223
|
return {
|
|
167
224
|
success: true,
|
|
168
225
|
result: { version: ConfigPrinter.getVersion() },
|
|
@@ -170,61 +227,135 @@ class ServeCommand {
|
|
|
170
227
|
}
|
|
171
228
|
|
|
172
229
|
/**
|
|
173
|
-
* Handle
|
|
230
|
+
* Handle initialize method
|
|
231
|
+
* Loads project config and creates a Transpiler instance
|
|
174
232
|
*/
|
|
175
|
-
private static
|
|
233
|
+
private static async handleInitialize(
|
|
176
234
|
params?: Record<string, unknown>,
|
|
177
|
-
): IMethodResult {
|
|
178
|
-
if (!params || typeof params.
|
|
235
|
+
): Promise<IMethodResult> {
|
|
236
|
+
if (!params || typeof params.workspacePath !== "string") {
|
|
179
237
|
return {
|
|
180
238
|
success: false,
|
|
181
239
|
errorCode: JsonRpcHandler.ERROR_INVALID_PARAMS,
|
|
182
|
-
errorMessage: "Missing required param:
|
|
240
|
+
errorMessage: "Missing required param: workspacePath",
|
|
183
241
|
};
|
|
184
242
|
}
|
|
185
243
|
|
|
186
|
-
const
|
|
244
|
+
const workspacePath = params.workspacePath;
|
|
245
|
+
ServeCommand.log(`initializing with workspace: ${workspacePath}`);
|
|
246
|
+
|
|
247
|
+
const config = ConfigLoader.load(workspacePath);
|
|
248
|
+
|
|
249
|
+
ServeCommand.transpiler = new Transpiler({
|
|
250
|
+
inputs: [],
|
|
251
|
+
includeDirs: config.include ?? [],
|
|
252
|
+
cppRequired: config.cppRequired ?? false,
|
|
253
|
+
target: config.target ?? "",
|
|
254
|
+
debugMode: config.debugMode ?? false,
|
|
255
|
+
noCache: config.noCache ?? false,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
ServeCommand.log(
|
|
259
|
+
`initialized (cppRequired=${config.cppRequired ?? false}, includeDirs=${(config.include ?? []).length})`,
|
|
260
|
+
);
|
|
187
261
|
|
|
188
262
|
return {
|
|
189
263
|
success: true,
|
|
190
|
-
result: {
|
|
191
|
-
success: result.success,
|
|
192
|
-
code: result.code,
|
|
193
|
-
errors: result.errors,
|
|
194
|
-
},
|
|
264
|
+
result: { success: true },
|
|
195
265
|
};
|
|
196
266
|
}
|
|
197
267
|
|
|
198
268
|
/**
|
|
199
|
-
* Handle
|
|
269
|
+
* Handle transpile method (called via _withSourceValidation wrapper)
|
|
270
|
+
* Uses full Transpiler for include resolution and C++ auto-detection
|
|
200
271
|
*/
|
|
201
|
-
private static
|
|
202
|
-
params
|
|
203
|
-
): IMethodResult {
|
|
204
|
-
if (!
|
|
272
|
+
private static async _handleTranspile(
|
|
273
|
+
params: ISourceParams,
|
|
274
|
+
): Promise<IMethodResult> {
|
|
275
|
+
if (!ServeCommand.transpiler) {
|
|
205
276
|
return {
|
|
206
277
|
success: false,
|
|
207
278
|
errorCode: JsonRpcHandler.ERROR_INVALID_PARAMS,
|
|
208
|
-
errorMessage: "
|
|
279
|
+
errorMessage: "Server not initialized. Call initialize first.",
|
|
209
280
|
};
|
|
210
281
|
}
|
|
211
282
|
|
|
212
|
-
const
|
|
283
|
+
const { source, filePath } = params;
|
|
284
|
+
|
|
285
|
+
const options = filePath
|
|
286
|
+
? { workingDir: dirname(filePath), sourcePath: filePath }
|
|
287
|
+
: undefined;
|
|
288
|
+
|
|
289
|
+
const result = await ServeCommand.transpiler.transpileSource(
|
|
290
|
+
source,
|
|
291
|
+
options,
|
|
292
|
+
);
|
|
213
293
|
|
|
214
294
|
return {
|
|
215
295
|
success: true,
|
|
216
296
|
result: {
|
|
217
297
|
success: result.success,
|
|
298
|
+
code: result.code,
|
|
218
299
|
errors: result.errors,
|
|
219
|
-
|
|
300
|
+
cppDetected: ServeCommand.transpiler.isCppDetected(),
|
|
220
301
|
},
|
|
221
302
|
};
|
|
222
303
|
}
|
|
223
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Handle parseSymbols method (called via _withSourceValidation wrapper)
|
|
307
|
+
* Runs full transpilation for include/C++ detection, then extracts symbols
|
|
308
|
+
* from the parse tree (preserving "extract symbols even with parse errors" behavior)
|
|
309
|
+
*/
|
|
310
|
+
private static async _handleParseSymbols(
|
|
311
|
+
params: ISourceParams,
|
|
312
|
+
): Promise<IMethodResult> {
|
|
313
|
+
const { source, filePath } = params;
|
|
314
|
+
|
|
315
|
+
// If transpiler is initialized, run transpileSource to trigger header
|
|
316
|
+
// resolution and C++ detection (results are discarded, we just want
|
|
317
|
+
// the side effects on the symbol table)
|
|
318
|
+
if (ServeCommand.transpiler && filePath) {
|
|
319
|
+
try {
|
|
320
|
+
await ServeCommand.transpiler.transpileSource(source, {
|
|
321
|
+
workingDir: dirname(filePath),
|
|
322
|
+
sourcePath: filePath,
|
|
323
|
+
});
|
|
324
|
+
} catch {
|
|
325
|
+
// Ignore transpilation errors - we still extract symbols below
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Delegate symbol extraction to parseWithSymbols (shared with WorkspaceIndex)
|
|
330
|
+
const result = parseWithSymbols(source);
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
success: true,
|
|
334
|
+
result,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Handle parseCHeader method (called via _withSourceValidation wrapper)
|
|
340
|
+
* Parses C/C++ header files and extracts symbols
|
|
341
|
+
*/
|
|
342
|
+
private static async _handleParseCHeader(
|
|
343
|
+
params: ISourceParams,
|
|
344
|
+
): Promise<IMethodResult> {
|
|
345
|
+
const { source, filePath } = params;
|
|
346
|
+
|
|
347
|
+
const result = parseCHeader(source, filePath);
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
success: true,
|
|
351
|
+
result,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
224
355
|
/**
|
|
225
356
|
* Handle shutdown method
|
|
226
357
|
*/
|
|
227
|
-
private static handleShutdown(): IMethodResult {
|
|
358
|
+
private static async handleShutdown(): Promise<IMethodResult> {
|
|
228
359
|
ServeCommand.shouldShutdown = true;
|
|
229
360
|
return {
|
|
230
361
|
success: true,
|
|
@@ -25,7 +25,7 @@ describe("ServeCommand", () => {
|
|
|
25
25
|
stdoutWriteSpy.mockRestore();
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
// Helper to send a request and capture the response
|
|
28
|
+
// Helper to send a request and capture the async response
|
|
29
29
|
async function sendRequest(request: object): Promise<object> {
|
|
30
30
|
const line = JSON.stringify(request);
|
|
31
31
|
// Access private handleLine method for testing
|
|
@@ -34,8 +34,14 @@ describe("ServeCommand", () => {
|
|
|
34
34
|
).handleLine.bind(ServeCommand);
|
|
35
35
|
handleLine(line);
|
|
36
36
|
|
|
37
|
-
//
|
|
38
|
-
|
|
37
|
+
// Wait for async dispatch to complete and write response
|
|
38
|
+
await vi.waitFor(() => {
|
|
39
|
+
expect(stdoutWriteSpy.mock.calls.length).toBeGreaterThan(0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Parse the response from the latest stdout.write call
|
|
43
|
+
const writeCall =
|
|
44
|
+
stdoutWriteSpy.mock.calls[stdoutWriteSpy.mock.calls.length - 1];
|
|
39
45
|
if (writeCall) {
|
|
40
46
|
const responseStr = writeCall[0] as string;
|
|
41
47
|
return JSON.parse(responseStr.trim());
|
|
@@ -54,8 +60,67 @@ describe("ServeCommand", () => {
|
|
|
54
60
|
});
|
|
55
61
|
});
|
|
56
62
|
|
|
63
|
+
describe("initialize", () => {
|
|
64
|
+
it("initializes with workspace path", async () => {
|
|
65
|
+
const response = await sendRequest({
|
|
66
|
+
id: 10,
|
|
67
|
+
method: "initialize",
|
|
68
|
+
params: { workspacePath: "/tmp" },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(response).toMatchObject({
|
|
72
|
+
id: 10,
|
|
73
|
+
result: { success: true },
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns error for missing workspacePath param", async () => {
|
|
78
|
+
const response = await sendRequest({
|
|
79
|
+
id: 11,
|
|
80
|
+
method: "initialize",
|
|
81
|
+
params: {},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(response).toMatchObject({
|
|
85
|
+
id: 11,
|
|
86
|
+
error: {
|
|
87
|
+
code: JsonRpcHandler.ERROR_INVALID_PARAMS,
|
|
88
|
+
message: "Missing required param: workspacePath",
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
57
94
|
describe("transpile", () => {
|
|
95
|
+
it("returns error when server not initialized", async () => {
|
|
96
|
+
// Reset transpiler to null by accessing private field
|
|
97
|
+
(ServeCommand as unknown as { transpiler: null }).transpiler = null;
|
|
98
|
+
stdoutWriteSpy.mockClear();
|
|
99
|
+
|
|
100
|
+
const response = await sendRequest({
|
|
101
|
+
id: 50,
|
|
102
|
+
method: "transpile",
|
|
103
|
+
params: { source: "u8 x <- 5;" },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(response).toMatchObject({
|
|
107
|
+
id: 50,
|
|
108
|
+
error: {
|
|
109
|
+
code: JsonRpcHandler.ERROR_INVALID_PARAMS,
|
|
110
|
+
message: "Server not initialized. Call initialize first.",
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
58
115
|
it("transpiles valid C-Next source", async () => {
|
|
116
|
+
// Initialize first (required for transpile)
|
|
117
|
+
await sendRequest({
|
|
118
|
+
id: 100,
|
|
119
|
+
method: "initialize",
|
|
120
|
+
params: { workspacePath: "/tmp" },
|
|
121
|
+
});
|
|
122
|
+
stdoutWriteSpy.mockClear();
|
|
123
|
+
|
|
59
124
|
const response = await sendRequest({
|
|
60
125
|
id: 2,
|
|
61
126
|
method: "transpile",
|
|
@@ -73,6 +138,14 @@ describe("ServeCommand", () => {
|
|
|
73
138
|
});
|
|
74
139
|
|
|
75
140
|
it("returns errors for invalid source", async () => {
|
|
141
|
+
// Initialize first
|
|
142
|
+
await sendRequest({
|
|
143
|
+
id: 101,
|
|
144
|
+
method: "initialize",
|
|
145
|
+
params: { workspacePath: "/tmp" },
|
|
146
|
+
});
|
|
147
|
+
stdoutWriteSpy.mockClear();
|
|
148
|
+
|
|
76
149
|
const response = await sendRequest({
|
|
77
150
|
id: 3,
|
|
78
151
|
method: "transpile",
|
|
@@ -139,6 +212,35 @@ describe("ServeCommand", () => {
|
|
|
139
212
|
);
|
|
140
213
|
});
|
|
141
214
|
|
|
215
|
+
it("parses symbols with filePath after initialization", async () => {
|
|
216
|
+
// Initialize first
|
|
217
|
+
await sendRequest({
|
|
218
|
+
id: 60,
|
|
219
|
+
method: "initialize",
|
|
220
|
+
params: { workspacePath: "/tmp" },
|
|
221
|
+
});
|
|
222
|
+
stdoutWriteSpy.mockClear();
|
|
223
|
+
|
|
224
|
+
const response = await sendRequest({
|
|
225
|
+
id: 61,
|
|
226
|
+
method: "parseSymbols",
|
|
227
|
+
params: {
|
|
228
|
+
source: "void myFunc() { }",
|
|
229
|
+
filePath: "/tmp/test.cnx",
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const result = response as {
|
|
234
|
+
id: number;
|
|
235
|
+
result: { success: boolean; symbols: Array<{ name: string }> };
|
|
236
|
+
};
|
|
237
|
+
expect(result.id).toBe(61);
|
|
238
|
+
expect(result.result.success).toBe(true);
|
|
239
|
+
expect(result.result.symbols).toEqual(
|
|
240
|
+
expect.arrayContaining([expect.objectContaining({ name: "myFunc" })]),
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
142
244
|
it("returns error for missing source param", async () => {
|
|
143
245
|
const response = await sendRequest({
|
|
144
246
|
id: 7,
|
|
@@ -154,6 +256,95 @@ describe("ServeCommand", () => {
|
|
|
154
256
|
},
|
|
155
257
|
});
|
|
156
258
|
});
|
|
259
|
+
|
|
260
|
+
it("returns error for missing params", async () => {
|
|
261
|
+
const response = await sendRequest({
|
|
262
|
+
id: 70,
|
|
263
|
+
method: "parseSymbols",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(response).toMatchObject({
|
|
267
|
+
id: 70,
|
|
268
|
+
error: {
|
|
269
|
+
code: JsonRpcHandler.ERROR_INVALID_PARAMS,
|
|
270
|
+
message: "Missing required param: source",
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("parseCHeader", () => {
|
|
277
|
+
it("parses symbols from C header source", async () => {
|
|
278
|
+
const response = await sendRequest({
|
|
279
|
+
id: 71,
|
|
280
|
+
method: "parseCHeader",
|
|
281
|
+
params: { source: "int myFunction(void);" },
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const result = response as {
|
|
285
|
+
id: number;
|
|
286
|
+
result: { success: boolean; symbols: Array<{ name: string }> };
|
|
287
|
+
};
|
|
288
|
+
expect(result.id).toBe(71);
|
|
289
|
+
expect(result.result.success).toBe(true);
|
|
290
|
+
expect(result.result.symbols).toEqual(
|
|
291
|
+
expect.arrayContaining([
|
|
292
|
+
expect.objectContaining({ name: "myFunction" }),
|
|
293
|
+
]),
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("parses struct definitions", async () => {
|
|
298
|
+
const response = await sendRequest({
|
|
299
|
+
id: 72,
|
|
300
|
+
method: "parseCHeader",
|
|
301
|
+
params: {
|
|
302
|
+
source: "typedef struct { int x; int y; } Point;",
|
|
303
|
+
filePath: "/tmp/types.h",
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const result = response as {
|
|
308
|
+
id: number;
|
|
309
|
+
result: { success: boolean; symbols: Array<{ name: string }> };
|
|
310
|
+
};
|
|
311
|
+
expect(result.id).toBe(72);
|
|
312
|
+
expect(result.result.success).toBe(true);
|
|
313
|
+
expect(result.result.symbols).toEqual(
|
|
314
|
+
expect.arrayContaining([expect.objectContaining({ name: "Point" })]),
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("returns error for missing source param", async () => {
|
|
319
|
+
const response = await sendRequest({
|
|
320
|
+
id: 73,
|
|
321
|
+
method: "parseCHeader",
|
|
322
|
+
params: {},
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
expect(response).toMatchObject({
|
|
326
|
+
id: 73,
|
|
327
|
+
error: {
|
|
328
|
+
code: JsonRpcHandler.ERROR_INVALID_PARAMS,
|
|
329
|
+
message: "Missing required param: source",
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("returns error for missing params", async () => {
|
|
335
|
+
const response = await sendRequest({
|
|
336
|
+
id: 74,
|
|
337
|
+
method: "parseCHeader",
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
expect(response).toMatchObject({
|
|
341
|
+
id: 74,
|
|
342
|
+
error: {
|
|
343
|
+
code: JsonRpcHandler.ERROR_INVALID_PARAMS,
|
|
344
|
+
message: "Missing required param: source",
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
});
|
|
157
348
|
});
|
|
158
349
|
|
|
159
350
|
describe("shutdown", () => {
|
|
@@ -197,11 +388,44 @@ describe("ServeCommand", () => {
|
|
|
197
388
|
handleLine("");
|
|
198
389
|
handleLine(" ");
|
|
199
390
|
|
|
391
|
+
// Wait a tick to ensure any async work would have started
|
|
392
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
393
|
+
|
|
200
394
|
// No response should be written for empty lines
|
|
201
395
|
expect(stdoutWriteSpy).not.toHaveBeenCalled();
|
|
202
396
|
});
|
|
203
397
|
});
|
|
204
398
|
|
|
399
|
+
describe("debug logging", () => {
|
|
400
|
+
it("writes debug messages to stderr when debug mode is enabled", async () => {
|
|
401
|
+
const stderrWriteSpy = vi
|
|
402
|
+
.spyOn(process.stderr, "write")
|
|
403
|
+
.mockImplementation(() => true);
|
|
404
|
+
|
|
405
|
+
// Enable debug mode via private field
|
|
406
|
+
(ServeCommand as unknown as { debugMode: boolean }).debugMode = true;
|
|
407
|
+
|
|
408
|
+
const response = await sendRequest({
|
|
409
|
+
id: 80,
|
|
410
|
+
method: "getVersion",
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
expect(response).toMatchObject({
|
|
414
|
+
id: 80,
|
|
415
|
+
result: { version: expect.any(String) },
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Debug logs should have been written to stderr
|
|
419
|
+
expect(stderrWriteSpy).toHaveBeenCalled();
|
|
420
|
+
const debugOutput = stderrWriteSpy.mock.calls.map((c) => c[0]).join("");
|
|
421
|
+
expect(debugOutput).toContain("[serve]");
|
|
422
|
+
|
|
423
|
+
// Clean up
|
|
424
|
+
(ServeCommand as unknown as { debugMode: boolean }).debugMode = false;
|
|
425
|
+
stderrWriteSpy.mockRestore();
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
205
429
|
describe("invalid JSON", () => {
|
|
206
430
|
it("returns parse error", async () => {
|
|
207
431
|
const handleLine = (
|