1
+ using System ;
1
2
using System . Linq ;
3
+ using System . Text ;
4
+ using System . Text . RegularExpressions ;
2
5
using Antlr4 . Runtime ;
3
6
using Antlr4 . Runtime . Tree ;
4
7
using Godot ;
8
+ using GodotExt ;
9
+ using OpenScadGraphEditor . Nodes ;
5
10
6
11
namespace OpenScadGraphEditor . Library . External
7
12
{
8
13
public class OpenScadVisitor : OpenScadParserBaseVisitor < object >
9
14
{
15
+ private readonly CommonTokenStream _commonTokenStream ;
10
16
private readonly ExternalReference _externalReference ;
11
17
private readonly string _sourceFileHash ;
12
18
13
19
14
- public OpenScadVisitor ( ExternalReference externalReference )
20
+ public OpenScadVisitor ( CommonTokenStream commonTokenStream , ExternalReference externalReference )
15
21
{
22
+ _commonTokenStream = commonTokenStream ;
16
23
_externalReference = externalReference ;
17
24
_sourceFileHash = _externalReference . IncludePath . SHA256Text ( ) ;
18
25
}
@@ -61,27 +68,288 @@ public override object VisitFunctionDeclaration(OpenScadParser.FunctionDeclarati
61
68
// first check if this function is not inside of any module
62
69
if ( IsNotInsideModule ( context ) )
63
70
{
64
- // for now we treat any parameter that is from an external function as PortType.ANY
65
- // same goes for the return type.
66
-
71
+
72
+
67
73
var functionName = context . identifier ( ) . IDENTIFIER ( ) . GetText ( ) ;
74
+ // return type of a function is always inferred as PortType.ANY unless overwritten by
75
+ // the documentation comment
68
76
var builder = FunctionBuilder . NewFunction ( functionName , MakeId ( "function" , functionName ) ) ;
69
77
70
78
// now find all the parameters
71
79
var parameterDeclarations = context . parameterList ( ) . parameterDeclaration ( ) ;
72
80
foreach ( var parameter in parameterDeclarations )
73
81
{
74
82
var name = parameter . identifier ( ) . IDENTIFIER ( ) . GetText ( ) ;
75
- builder . WithParameter ( name ) ;
83
+ // try to infer the parameter type.
84
+ var portType = InferParameterType ( parameter ) ;
85
+ builder . WithParameter ( name , portType ) ;
76
86
}
77
87
78
- _externalReference . Functions . Add ( builder . Build ( ) ) ;
88
+ var comment = ParseDocumentationComment ( context . FUNCTION ( ) ) ;
89
+
90
+ var functionDescription = builder . Build ( ) ;
91
+ // apply any documentation comment
92
+ comment . ApplyTo ( functionDescription ) ;
93
+
94
+ _externalReference . Functions . Add ( functionDescription ) ;
79
95
}
80
96
81
97
// and walk the rest of the tree
82
98
return base . VisitFunctionDeclaration ( context ) ;
83
99
}
84
100
101
+ private PortType InferParameterType ( OpenScadParser . ParameterDeclarationContext parameterDeclarationContext )
102
+ {
103
+ var expression = parameterDeclarationContext . expression ( ) ;
104
+ if ( expression == null )
105
+ {
106
+ return PortType . Any ;
107
+ }
108
+
109
+ OpenScadParser . ExpressionContext StripParentheses ( OpenScadParser . ExpressionContext outer )
110
+ {
111
+ while ( outer . parenthesizedExpression ( ) != null )
112
+ {
113
+ outer = outer . parenthesizedExpression ( ) . expression ( ) ;
114
+ }
115
+
116
+ return outer ;
117
+ }
118
+
119
+ expression = StripParentheses ( expression ) ;
120
+
121
+ // is it a simple expression?
122
+ var simpleExpression = expression . simpleExpression ( ) ;
123
+ if ( simpleExpression ? . NUMBER ( ) != null )
124
+ {
125
+ return PortType . Number ;
126
+ }
127
+
128
+ if ( simpleExpression ? . STRING ( ) != null )
129
+ {
130
+ return PortType . String ;
131
+ }
132
+
133
+ if ( simpleExpression ? . BOOLEAN ( ) != null )
134
+ {
135
+ return PortType . Boolean ;
136
+ }
137
+
138
+ // is it a vector expression?
139
+ var vectorExpression = expression . vectorExpression ( ) ;
140
+ if ( vectorExpression != null )
141
+ {
142
+ // [ VectorInner, VectorInner, VectorInner ]
143
+ // count how many vectorInner we have
144
+ var vectorContents = vectorExpression . children . OfType < OpenScadParser . VectorInnerContext > ( ) . ToList ( ) ;
145
+ if ( vectorContents . Count > 3 )
146
+ {
147
+ // we don't care what is inside, this is an array
148
+ return PortType . Array ;
149
+ }
150
+
151
+ // now check that all vector contents are ultimately numbers.
152
+ foreach ( var vectorInner in vectorContents )
153
+ {
154
+ // [ VectorInner { ParenthesizedVectorInner { ... expression ... } } ]
155
+ var innerExpression = vectorInner ;
156
+ while ( innerExpression . parenthesizedVectorInner ( ) != null )
157
+ {
158
+ innerExpression = vectorInner . parenthesizedVectorInner ( ) . vectorInner ( ) ;
159
+ }
160
+
161
+ // now we should have all parenthesized vectorInner removed.
162
+ var innermostExpression = StripParentheses ( innerExpression . expression ( ) ) ;
163
+ if ( innermostExpression . simpleExpression ( ) ? . NUMBER ( ) == null )
164
+ {
165
+ // if the expression is anything but a number, we infer as "Array"
166
+ return PortType . Array ;
167
+ }
168
+ }
169
+
170
+ // if we get here, we have a vector of numbers
171
+ if ( vectorContents . Count == 2 )
172
+ {
173
+ return PortType . Vector2 ;
174
+ }
175
+
176
+ return PortType . Vector3 ;
177
+ }
178
+
179
+ // anything else, we don't recognize -> Any
180
+ return PortType . Any ;
181
+ }
182
+
183
+ private DocumentationComment ParseDocumentationComment ( ISyntaxTree invokableDeclarationStart )
184
+ {
185
+
186
+ var hiddenTokensToLeft = _commonTokenStream
187
+ . GetHiddenTokensToLeft ( invokableDeclarationStart . SourceInterval . a , 2 /* channel 2 == comments */ ) ;
188
+
189
+ // that is a really bad API, a list should never return null but apparently from the source code
190
+ // it does.
191
+ if ( hiddenTokensToLeft == null )
192
+ {
193
+ // return an empty comment
194
+ return new DocumentationComment ( ) ;
195
+ }
196
+
197
+ string CleanUpCommentText ( string text )
198
+ {
199
+ // remove any starting /** and whitespace or newlines
200
+ text = text . TrimStart ( '/' , '*' , ' ' , '\n ' , '\r ' , '\t ' ) ;
201
+ // remove any ending */ and whitespace or newlines
202
+ text = text . TrimEnd ( '/' , '*' , ' ' , '\n ' , '\r ' , '\t ' ) ;
203
+
204
+ // for every line in the text, remove leading * and all whitespace before the *.
205
+ // do not remove whitespace after the *, because that is intended indentation.
206
+ var lines = text . Split ( '\n ' ) ;
207
+ var result = new StringBuilder ( ) ;
208
+ foreach ( var line in lines )
209
+ {
210
+ var trimmedLine = line . TrimStart ( '*' , ' ' , '\t ' ) ;
211
+ // we deliberately do not use AppendLine here as we always use \n as line separator internally.
212
+ result . Append ( trimmedLine ) . Append ( "\n " ) ;
213
+ }
214
+
215
+ // remove the last newline
216
+ result . Length -- ;
217
+
218
+ return result . ToString ( ) ;
219
+ }
220
+
221
+ // check the returned list backwards and find the first documentation comment (e.g. the one that is closest
222
+ // to the invokable declaration)
223
+ foreach ( var token in hiddenTokensToLeft . Reverse ( ) )
224
+ {
225
+ if ( token . Type != OpenScadParser . BLOCK_COMMENT || ! token . Text . StartsWith ( "/**" ) )
226
+ {
227
+ continue ; // not a documentation comment
228
+ }
229
+
230
+ // a documentation comment starts with the text, and then there may be @param and @return tags
231
+ // in any order. The text after each tag belongs in full to the preceding tag. So we need to
232
+ // split the text into the tags and then parse the tags separately.
233
+
234
+ const int paramTag = 1 ;
235
+ const int returnTag = 2 ;
236
+ // we declare the regexes here to avoid the overhead of creating them every time we need them
237
+ var paramRegex = new Regex ( @"@param\s+(?<name>\w+)\s*(?<type>\[\w+\])?\s+(?<description>.*)" , RegexOptions . Singleline ) ;
238
+ var returnRegex = new Regex ( @"@return\s+(?<type>\[\w+\])?\s+(?<description>.*)" , RegexOptions . Singleline ) ;
239
+
240
+
241
+ // Helper function that finds the next tag in the text and its type.
242
+ bool HasNextTag ( string text , int startIndex , out int resultIndex , out int tagType )
243
+ {
244
+ resultIndex = text . IndexOf ( "@param" , startIndex , StringComparison . Ordinal ) ;
245
+ if ( resultIndex == - 1 )
246
+ {
247
+ resultIndex = text . IndexOf ( "@return" , startIndex , StringComparison . Ordinal ) ;
248
+ if ( resultIndex == - 1 )
249
+ {
250
+ tagType = default ;
251
+ return false ;
252
+ }
253
+ tagType = returnTag ;
254
+ return true ;
255
+ }
256
+
257
+ tagType = paramTag ;
258
+ return true ;
259
+ }
260
+
261
+ // helper function that converts a type string into a PortType
262
+ PortType GetPortType ( string typeString )
263
+ {
264
+ if ( typeString == null )
265
+ {
266
+ return PortType . None ;
267
+ }
268
+
269
+ switch ( typeString )
270
+ {
271
+ case "[number]" :
272
+ return PortType . Number ;
273
+ case "[string]" :
274
+ return PortType . String ;
275
+ case "[any]" :
276
+ return PortType . Any ;
277
+ case "[boolean]" :
278
+ return PortType . Boolean ;
279
+ case "[vector2]" :
280
+ return PortType . Vector2 ;
281
+ case "[vector3]" :
282
+ return PortType . Vector3 ;
283
+ case "[array]" :
284
+ return PortType . Array ;
285
+ }
286
+
287
+ // anything else or something we don't recognize
288
+ return PortType . None ;
289
+ }
290
+
291
+ if ( ! HasNextTag ( token . Text , 0 , out var nextIndex , out var nextTagType ) ) {
292
+ // there is no next tag, so we can use the whole text
293
+ return new DocumentationComment { Summary = CleanUpCommentText ( token . Text ) } ;
294
+ }
295
+
296
+ // everything until now is the summary
297
+ var result = new DocumentationComment
298
+ {
299
+ Summary = CleanUpCommentText ( token . Text . Substring ( 0 , nextIndex ) )
300
+ } ;
301
+
302
+ do
303
+ {
304
+ var hasNext = HasNextTag ( token . Text , nextIndex + 1 , out var endIndex , out var followingTagType ) ;
305
+ if ( ! hasNext )
306
+ {
307
+ endIndex = token . Text . Length - 1 ;
308
+ }
309
+
310
+ // now extract the tag text
311
+ var tagText = token . Text . Substring ( nextIndex , endIndex - nextIndex ) ;
312
+
313
+ // check the tag type and try to parse it.
314
+ switch ( nextTagType )
315
+ {
316
+ case paramTag :
317
+ var paramMatch = paramRegex . Match ( tagText ) ;
318
+ if ( paramMatch . Success )
319
+ {
320
+ result . AddParameter (
321
+ paramMatch . Groups [ "name" ] ? . Value ?? "" ,
322
+ CleanUpCommentText ( paramMatch . Groups [ "description" ] ? . Value ?? "" ) ,
323
+ GetPortType ( paramMatch . Groups [ "type" ] ? . Value )
324
+ ) ;
325
+ }
326
+ // if we can't parse it properly, ignore it.
327
+ break ;
328
+ case returnTag :
329
+ var returnMatch = returnRegex . Match ( tagText ) ;
330
+ if ( returnMatch . Success )
331
+ {
332
+ result . ReturnValueTypeHint = GetPortType ( returnMatch . Groups [ "type" ] ? . Value ) ;
333
+ result . ReturnValueDescription = CleanUpCommentText ( returnMatch . Groups [ "description" ] ? . Value ?? "" ) ;
334
+ }
335
+ // if we can't parse it properly, ignore it.
336
+ break ;
337
+ }
338
+
339
+ if ( ! hasNext )
340
+ {
341
+ return result ;
342
+ }
343
+
344
+ nextIndex = endIndex ;
345
+ nextTagType = followingTagType ;
346
+ } while ( true ) ;
347
+ }
348
+
349
+ // no documentation comment found at all, return an empty one
350
+ return new DocumentationComment ( ) ;
351
+ }
352
+
85
353
86
354
public override object VisitModuleDeclaration ( OpenScadParser . ModuleDeclarationContext context )
87
355
{
@@ -96,15 +364,20 @@ public override object VisitModuleDeclaration(OpenScadParser.ModuleDeclarationCo
96
364
foreach ( var parameter in parameterDeclarations )
97
365
{
98
366
var name = parameter . identifier ( ) . IDENTIFIER ( ) . GetText ( ) ;
99
- builder . WithParameter ( name ) ;
367
+ var portType = InferParameterType ( parameter ) ;
368
+ builder . WithParameter ( name , portType ) ;
100
369
}
101
- // finally check if this module supports children
370
+ // check if this module supports children
102
371
if ( SupportsChildren ( context ) )
103
372
{
104
373
builder . WithChildren ( ) ;
105
374
}
106
- _externalReference . Modules . Add ( builder . Build ( ) ) ;
107
-
375
+
376
+ var moduleDescription = builder . Build ( ) ;
377
+ // parse any documentation comment
378
+ var documentationComment = ParseDocumentationComment ( context . MODULE ( ) ) ;
379
+ documentationComment . ApplyTo ( moduleDescription ) ;
380
+ _externalReference . Modules . Add ( moduleDescription ) ;
108
381
}
109
382
110
383
// and walk the rest of the tree
0 commit comments