1 | // Copyright (C) 2021 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
3 | |
4 | #include "qqmldomcomments_p.h" |
5 | #include "qqmldomoutwriter_p.h" |
6 | #include "qqmldomlinewriter_p.h" |
7 | #include "qqmldomelements_p.h" |
8 | #include "qqmldomexternalitems_p.h" |
9 | #include "qqmldomastdumper_p.h" |
10 | #include "qqmldomattachedinfo_p.h" |
11 | |
12 | #include <QtQml/private/qqmljsastvisitor_p.h> |
13 | #include <QtQml/private/qqmljsast_p.h> |
14 | #include <QtQml/private/qqmljslexer_p.h> |
15 | |
16 | #include <QtCore/QSet> |
17 | |
18 | #include <variant> |
19 | |
20 | static Q_LOGGING_CATEGORY(, "qt.qmldom.comments" , QtWarningMsg); |
21 | |
22 | QT_BEGIN_NAMESPACE |
23 | namespace QQmlJS { |
24 | namespace Dom { |
25 | |
26 | /*! |
27 | \internal |
28 | \class QQmlJS::Dom::AstComments |
29 | |
30 | \brief Associates comments with AST::Node * |
31 | |
32 | Comments are associated to the largest closest node with the |
33 | following algorithm: |
34 | \list |
35 | \li comments of a node can either be preComments or postComments (before |
36 | or after the element) |
37 | \li define start and end for each element, if two elements start (or end) |
38 | at the same place the first (larger) wins. |
39 | \li associate the comments either with the element just before or |
40 | just after unless the comments is *inside* an element (meaning that |
41 | going back there is a start before finding an end, or going forward an |
42 | end is met before a start). |
43 | \li to choose between the element before or after, we look at the start |
44 | of the comment, if it is on a new line then associating it as |
45 | preComment to the element after is preferred, otherwise post comment |
46 | of the previous element (inline element). |
47 | This is the only space dependent choice, making comment assignment |
48 | quite robust |
49 | \li if the comment is intrinsically inside all elements then it is moved |
50 | to before the smallest element. |
51 | This is the largest reorganization performed, and it is still quite |
52 | small and difficult to trigger. |
53 | \li the comments are stored with the whitespace surrounding them, from |
54 | the preceding newline (and recording if a newline is required before |
55 | it) until the newline after. |
56 | This allows a better reproduction of the comments. |
57 | \endlist |
58 | */ |
59 | /*! |
60 | \class QQmlJS::Dom::CommentInfo |
61 | |
62 | \brief Extracts various pieces and information out of a rawComment string |
63 | |
64 | Comments store a string (rawComment) with comment characters (//,..) and spaces. |
65 | Sometime one wants just the comment, the commentcharacters, the space before the comment,.... |
66 | CommentInfo gets such a raw comment string and makes the various pieces available |
67 | */ |
68 | CommentInfo::(QStringView ) : rawComment(rawComment) |
69 | { |
70 | commentBegin = 0; |
71 | while (commentBegin < quint32(rawComment.size()) && rawComment.at(n: commentBegin).isSpace()) { |
72 | if (rawComment.at(n: commentBegin) == QLatin1Char('\n')) |
73 | hasStartNewline = true; |
74 | ++commentBegin; |
75 | } |
76 | if (commentBegin < quint32(rawComment.size())) { |
77 | QString expectedEnd; |
78 | switch (rawComment.at(n: commentBegin).unicode()) { |
79 | case '/': |
80 | commentStartStr = rawComment.mid(pos: commentBegin, n: 2); |
81 | if (commentStartStr == u"/*" ) { |
82 | expectedEnd = QStringLiteral(u"*/" ); |
83 | } else { |
84 | if (commentStartStr == u"//" ) { |
85 | expectedEnd = QStringLiteral(u"\n" ); |
86 | } else { |
87 | warnings.append(t: tr(sourceText: "Unexpected comment start %1" ).arg(a: commentStartStr)); |
88 | } |
89 | } |
90 | break; |
91 | case '#': |
92 | commentStartStr = rawComment.mid(pos: commentBegin, n: 1); |
93 | expectedEnd = QStringLiteral(u"\n" ); |
94 | break; |
95 | default: |
96 | commentStartStr = rawComment.mid(pos: commentBegin, n: 1); |
97 | warnings.append(t: tr(sourceText: "Unexpected comment start %1" ).arg(a: commentStartStr)); |
98 | break; |
99 | } |
100 | commentEnd = commentBegin + commentStartStr.size(); |
101 | quint32 rawEnd = quint32(rawComment.size()); |
102 | commentContentEnd = commentContentBegin = commentEnd; |
103 | QChar e1 = ((expectedEnd.isEmpty()) ? QChar::fromLatin1(c: 0) : expectedEnd.at(i: 0)); |
104 | while (commentEnd < rawEnd) { |
105 | QChar c = rawComment.at(n: commentEnd); |
106 | if (c == e1) { |
107 | if (expectedEnd.size() > 1) { |
108 | if (++commentEnd < rawEnd && rawComment.at(n: commentEnd) == expectedEnd.at(i: 1)) { |
109 | Q_ASSERT(expectedEnd.size() == 2); |
110 | commentEndStr = rawComment.mid(pos: ++commentEnd - 2, n: 2); |
111 | break; |
112 | } else { |
113 | commentContentEnd = commentEnd; |
114 | } |
115 | } else { |
116 | // Comment ends with \n, treat as it is not part of the comment but post whitespace |
117 | commentEndStr = rawComment.mid(pos: commentEnd - 1, n: 1); |
118 | break; |
119 | } |
120 | } else if (!c.isSpace()) { |
121 | commentContentEnd = commentEnd; |
122 | } else if (c == QLatin1Char('\n')) { |
123 | ++nContentNewlines; |
124 | } else if (c == QLatin1Char('\r')) { |
125 | if (expectedEnd == QStringLiteral(u"\n" )) { |
126 | if (commentEnd + 1 < rawEnd |
127 | && rawComment.at(n: commentEnd + 1) == QLatin1Char('\n')) { |
128 | ++commentEnd; |
129 | commentEndStr = rawComment.mid(pos: ++commentEnd - 2, n: 2); |
130 | } else { |
131 | commentEndStr = rawComment.mid(pos: ++commentEnd - 1, n: 1); |
132 | } |
133 | break; |
134 | } else if (commentEnd + 1 == rawEnd |
135 | || rawComment.at(n: commentEnd + 1) != QLatin1Char('\n')) { |
136 | ++nContentNewlines; |
137 | } |
138 | } |
139 | ++commentEnd; |
140 | } |
141 | |
142 | if (commentEnd > 0 |
143 | && (rawComment.at(n: commentEnd - 1) == QLatin1Char('\n') |
144 | || rawComment.at(n: commentEnd - 1) == QLatin1Char('\r'))) |
145 | hasEndNewline = true; |
146 | quint32 i = commentEnd; |
147 | while (i < rawEnd && rawComment.at(n: i).isSpace()) { |
148 | if (rawComment.at(n: i) == QLatin1Char('\n') || rawComment.at(n: i) == QLatin1Char('\r')) |
149 | hasEndNewline = true; |
150 | ++i; |
151 | } |
152 | if (i < rawEnd) { |
153 | warnings.append(t: tr(sourceText: "Non whitespace char %1 after comment end at %2" ) |
154 | .arg(a: rawComment.at(n: i)) |
155 | .arg(a: i)); |
156 | } |
157 | } |
158 | } |
159 | |
160 | /*! |
161 | \class QQmlJS::Dom::Comment |
162 | |
163 | \brief Represents a comment |
164 | |
165 | Comments are not needed for execute the program, so they are aimed to the programmer, |
166 | and have few functions: explaining code, adding extra info/context (who did write, |
167 | when licensing,...) or disabling code. |
168 | Being for the programmer and being non functional it is difficult to treat them properly. |
169 | So preserving them as much as possible is the best course of action. |
170 | |
171 | To acheive this comment is represented by |
172 | \list |
173 | \li newlinesBefore: the number of newlines before the comment, to preserve spacing between |
174 | comments (the extraction routines limit this to 2 at most, i.e. a single empty line) \li |
175 | rawComment: a string with the actual comment including whitespace before and after and the |
176 | comment characters (whitespace before is limited to spaces/tabs to preserve indentation or |
177 | spacing just before starting the comment) \endlist The rawComment is a bit annoying if one wants |
178 | to change the comment, or extract information from it. For this reason info gives access to the |
179 | various elements of it: the comment characters #, // or / |
180 | *, the space before it, and the actual comment content. |
181 | |
182 | the comments are stored with the whitespace surrounding them, from |
183 | the preceding newline (and recording if a newline is required before |
184 | it) until the newline after. |
185 | |
186 | A comment has methods to write it out again (write) and expose it to the Dom |
187 | (iterateDirectSubpaths). |
188 | */ |
189 | |
190 | /*! |
191 | \brief Expose attributes to the Dom |
192 | */ |
193 | bool Comment::(DomItem &self, DirectVisitor visitor) |
194 | { |
195 | bool cont = true; |
196 | cont = cont && self.dvValueField(visitor, f: Fields::rawComment, value: rawComment()); |
197 | cont = cont && self.dvValueField(visitor, f: Fields::newlinesBefore, value: newlinesBefore()); |
198 | return cont; |
199 | } |
200 | |
201 | void Comment::(OutWriter &lw, SourceLocation *) const |
202 | { |
203 | if (newlinesBefore()) |
204 | lw.ensureNewline(nNewlines: newlinesBefore()); |
205 | CommentInfo cInfo = info(); |
206 | lw.ensureSpace(space: cInfo.preWhitespace()); |
207 | QStringView cBody = cInfo.comment(); |
208 | PendingSourceLocationId cLoc = lw.lineWriter.startSourceLocation(commentLocation); |
209 | lw.write(v: cBody.mid(pos: 0, n: 1)); |
210 | bool indentOn = lw.indentNextlines; |
211 | lw.indentNextlines = false; |
212 | lw.write(v: cBody.mid(pos: 1)); |
213 | lw.indentNextlines = indentOn; |
214 | lw.lineWriter.endSourceLocation(cLoc); |
215 | lw.write(v: cInfo.postWhitespace()); |
216 | } |
217 | |
218 | /*! |
219 | \class QQmlJS::Dom::CommentedElement |
220 | \brief Keeps the comment associated with an element |
221 | |
222 | A comment can be attached to an element (that is always a range of the file with a start and |
223 | end) only in two ways: it can precede the region (preComments), or follow it (postComments). |
224 | */ |
225 | |
226 | /*! |
227 | \class QQmlJS::Dom::RegionComments |
228 | \brief Keeps the comments associated with a DomItem |
229 | |
230 | A DomItem can be more complex that just a start/end, it can have multiple regions, for example |
231 | a return or a function token might define a region. |
232 | The empty string is the region that represents the whole element. |
233 | |
234 | Every region has a name, and should be written out using the OutWriter.writeRegion (or |
235 | startRegion/ EndRegion). Region comments keeps a mapping containing them. |
236 | */ |
237 | |
238 | bool CommentedElement::(DomItem &self, DirectVisitor visitor) |
239 | { |
240 | bool cont = true; |
241 | cont = cont && self.dvWrapField(visitor, f: Fields::preComments, obj&: preComments); |
242 | cont = cont && self.dvWrapField(visitor, f: Fields::postComments, obj&: postComments); |
243 | return cont; |
244 | } |
245 | |
246 | void CommentedElement::(OutWriter &lw, QList<SourceLocation> *locs) const |
247 | { |
248 | if (locs) |
249 | locs->resize(size: preComments.size()); |
250 | int i = 0; |
251 | for (const Comment &c : preComments) |
252 | c.write(lw, commentLocation: (locs ? &((*locs)[i++]) : nullptr)); |
253 | } |
254 | |
255 | void CommentedElement::(OutWriter &lw, QList<SourceLocation> *locs) const |
256 | { |
257 | if (locs) |
258 | locs->resize(size: postComments.size()); |
259 | int i = 0; |
260 | for (const Comment &c : postComments) |
261 | c.write(lw, commentLocation: (locs ? &((*locs)[i++]) : nullptr)); |
262 | } |
263 | |
264 | /*! |
265 | \brief Given the SourceLocation of the current element returns the comments associated with the |
266 | start and end of item |
267 | |
268 | The map uses an index that is based on 2*the location. Thus for every location l it is possible |
269 | to have two indexes: 2*l (just before) and 2*l+1 (just after). |
270 | This allows to attach comments to indexes representing either just before or after any location |
271 | */ |
272 | QMultiMap<quint32, const QList<Comment> *> |
273 | CommentedElement::(SourceLocation elLocation) const |
274 | { |
275 | return QMultiMap<quint32, const QList<Comment> *>( |
276 | { { elLocation.begin() * 2, &preComments }, |
277 | { elLocation.end() * 2 + 1, &postComments } }); |
278 | } |
279 | |
280 | using namespace QQmlJS::AST; |
281 | |
282 | class RegionRef |
283 | { |
284 | public: |
285 | Path path; // store the MutableDomItem instead? |
286 | QString regionName; |
287 | }; |
288 | |
289 | // internal class to keep a reference either to an AST::Node* or a region of a DomItem and the |
290 | // size of that region |
291 | class ElementRef |
292 | { |
293 | public: |
294 | ElementRef(AST::Node *node, quint32 size) : element(node), size(size) { } |
295 | ElementRef(Path path, QString region, quint32 size) |
296 | : element(RegionRef { .path: path, .regionName: region }), size(size) |
297 | { |
298 | } |
299 | operator bool() const |
300 | { |
301 | return (element.index() == 0 && std::get<0>(v: element)) || element.index() == 1 || size != 0; |
302 | } |
303 | ElementRef() = default; |
304 | |
305 | std::variant<AST::Node *, RegionRef> element; |
306 | quint32 size = 0; |
307 | }; |
308 | |
309 | /*! |
310 | \class QQmlJS::Dom::VisitAll |
311 | \brief A vistor that visits all the AST:Node |
312 | |
313 | The default visitor does not necessarily visit all nodes, because some part |
314 | of the AST are typically handled manually. This visitor visits *all* AST |
315 | elements contained. |
316 | |
317 | Note: Subclasses should take care to call the parent (i.e. this) visit/endVisit |
318 | methods when overriding them, to guarantee that all element are really visited |
319 | */ |
320 | |
321 | /*! |
322 | returns a set with all Ui* Nodes (i.e. the top level non javascript Qml) |
323 | */ |
324 | QSet<int> VisitAll::uiKinds() |
325 | { |
326 | static QSet<int> res({ AST::Node::Kind_UiObjectMemberList, AST::Node::Kind_UiArrayMemberList, |
327 | AST::Node::Kind_UiParameterList, AST::Node::Kind_UiHeaderItemList, |
328 | AST::Node::Kind_UiEnumMemberList, AST::Node::Kind_UiAnnotationList, |
329 | |
330 | AST::Node::Kind_UiArrayBinding, AST::Node::Kind_UiImport, |
331 | AST::Node::Kind_UiObjectBinding, AST::Node::Kind_UiObjectDefinition, |
332 | AST::Node::Kind_UiInlineComponent, AST::Node::Kind_UiObjectInitializer, |
333 | #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) |
334 | AST::Node::Kind_UiPragmaValueList, |
335 | #endif |
336 | AST::Node::Kind_UiPragma, AST::Node::Kind_UiProgram, |
337 | AST::Node::Kind_UiPublicMember, AST::Node::Kind_UiQualifiedId, |
338 | AST::Node::Kind_UiScriptBinding, AST::Node::Kind_UiSourceElement, |
339 | AST::Node::Kind_UiEnumDeclaration, AST::Node::Kind_UiVersionSpecifier, |
340 | AST::Node::Kind_UiRequired, AST::Node::Kind_UiAnnotation }); |
341 | return res; |
342 | } |
343 | |
344 | // internal private class to set all the starts/ends of the nodes/regions |
345 | class AstRangesVisitor final : protected VisitAll |
346 | { |
347 | public: |
348 | AstRangesVisitor() = default; |
349 | |
350 | void addNodeRanges(AST::Node *rootNode); |
351 | void addItemRanges(DomItem item, FileLocations::Tree itemLocations, Path currentP); |
352 | |
353 | void throwRecursionDepthError() override { } |
354 | |
355 | static const QSet<int> kindsToSkip(); |
356 | |
357 | bool preVisit(Node *n) override |
358 | { |
359 | if (!kindsToSkip().contains(value: n->kind)) { |
360 | quint32 start = n->firstSourceLocation().begin(); |
361 | quint32 end = n->lastSourceLocation().end(); |
362 | if (!starts.contains(key: start)) |
363 | starts.insert(key: start, value: { n, end - start }); |
364 | if (!ends.contains(key: end)) |
365 | ends.insert(key: end, value: { n, end - start }); |
366 | } |
367 | return true; |
368 | } |
369 | |
370 | QQmlJS::Engine *engine; |
371 | FileLocations::Tree rootItemLocations; |
372 | QMap<quint32, ElementRef> starts; |
373 | QMap<quint32, ElementRef> ends; |
374 | }; |
375 | |
376 | void AstRangesVisitor::addNodeRanges(AST::Node *rootNode) |
377 | { |
378 | AST::Node::accept(node: rootNode, visitor: this); |
379 | } |
380 | |
381 | void AstRangesVisitor::addItemRanges(DomItem item, FileLocations::Tree itemLocations, Path currentP) |
382 | { |
383 | if (!itemLocations) { |
384 | if (item) |
385 | qCWarning(commentsLog) << "reached item" << item.canonicalPath() << "without locations" ; |
386 | return; |
387 | } |
388 | DomItem = item.field(name: Fields::comments); |
389 | if (comments) { |
390 | auto regs = itemLocations->info().regions; |
391 | for (auto it = regs.cbegin(), end = regs.cend(); it != end; ++it) { |
392 | quint32 startI = it.value().begin(); |
393 | quint32 endI = it.value().end(); |
394 | if (!starts.contains(key: startI)) |
395 | starts.insert(key: startI, value: { currentP, it.key(), quint32(endI - startI) }); |
396 | if (!ends.contains(key: endI)) |
397 | ends.insert(key: endI, value: { currentP, it.key(), endI - startI }); |
398 | } |
399 | } |
400 | { |
401 | auto subMaps = itemLocations->subItems(); |
402 | for (auto it = subMaps.begin(), end = subMaps.end(); it != end; ++it) { |
403 | addItemRanges(item: item.path(p: it.key()), |
404 | itemLocations: std::static_pointer_cast<AttachedInfoT<FileLocations>>(r: it.value()), |
405 | currentP: currentP.path(toAdd: it.key())); |
406 | } |
407 | } |
408 | } |
409 | |
410 | const QSet<int> AstRangesVisitor::kindsToSkip() |
411 | { |
412 | static QSet<int> res = QSet<int>({ |
413 | AST::Node::Kind_ArgumentList, |
414 | AST::Node::Kind_ElementList, |
415 | AST::Node::Kind_FormalParameterList, |
416 | AST::Node::Kind_ImportsList, |
417 | AST::Node::Kind_ExportsList, |
418 | AST::Node::Kind_PropertyDefinitionList, |
419 | AST::Node::Kind_StatementList, |
420 | AST::Node::Kind_VariableDeclarationList, |
421 | AST::Node::Kind_ClassElementList, |
422 | AST::Node::Kind_PatternElementList, |
423 | AST::Node::Kind_PatternPropertyList, |
424 | AST::Node::Kind_TypeArgument, |
425 | }) |
426 | .unite(other: VisitAll::uiKinds()); |
427 | return res; |
428 | } |
429 | |
430 | /*! |
431 | \class QQmlJS::Dom::AstComments |
432 | \brief Stores the comments associated with javascript AST::Node pointers |
433 | */ |
434 | |
435 | bool AstComments::(DomItem &self, DirectVisitor visitor) |
436 | { |
437 | bool cont = self.dvItemField(visitor, f: Fields::commentedElements, it: [this, &self]() { |
438 | return self.subMapItem(map: Map( |
439 | self.pathFromOwner().field(name: Fields::commentedElements), |
440 | [this](DomItem &map, QString key) { |
441 | bool ok; |
442 | // we expose the comments as map just for debugging purposes, |
443 | // as key we use the address hex value as key (keys must be strings) |
444 | quintptr v = key.split(sep: QLatin1Char('_')).last().toULong(ok: &ok, base: 16); |
445 | // recover the actual key, and check if it is in the map |
446 | AST::Node *n = reinterpret_cast<AST::Node *>(v); |
447 | if (ok && m_commentedElements.contains(key: n)) |
448 | return map.wrap(c: PathEls::Key(key), obj&: m_commentedElements[n]); |
449 | return DomItem(); |
450 | }, |
451 | [this](DomItem &) { |
452 | QSet<QString> res; |
453 | for (AST::Node *n : m_commentedElements.keys()) { |
454 | QString name; |
455 | if (n) |
456 | name = QString::number(n->kind); // we should add mapping to |
457 | // string for this |
458 | res.insert(value: name + QStringLiteral(u"_" ) + QString::number(quintptr(n), base: 16)); |
459 | } |
460 | return res; |
461 | }, |
462 | QLatin1String("CommentedElements" ))); |
463 | }); |
464 | return cont; |
465 | } |
466 | |
467 | void AstComments::(MutableDomItem &item) |
468 | { |
469 | if (std::shared_ptr<ScriptExpression> scriptPtr = item.ownerAs<ScriptExpression>()) { |
470 | DomItem itemItem = item.item(); |
471 | return collectComments(engine: scriptPtr->engine(), n: scriptPtr->ast(), collectComments: scriptPtr->astComments(), |
472 | rootItem: item, rootItemLocations: FileLocations::treeOf(itemItem)); |
473 | } else if (std::shared_ptr<QmlFile> qmlFilePtr = item.ownerAs<QmlFile>()) { |
474 | return collectComments(engine: qmlFilePtr->engine(), n: qmlFilePtr->ast(), collectComments: qmlFilePtr->astComments(), |
475 | rootItem: item, rootItemLocations: qmlFilePtr->fileLocationsTree()); |
476 | } else { |
477 | qCWarning(commentsLog) |
478 | << "collectComments works with QmlFile and ScriptExpression, not with" |
479 | << item.internalKindStr(); |
480 | } |
481 | } |
482 | |
483 | /*! |
484 | \brief |
485 | Collects and associates comments with javascript AST::Node pointers and MutableDomItem in |
486 | rootItem |
487 | */ |
488 | void AstComments::(std::shared_ptr<Engine> engine, AST::Node *n, |
489 | std::shared_ptr<AstComments> ccomm, MutableDomItem rootItem, |
490 | FileLocations::Tree rootItemLocations) |
491 | { |
492 | if (!n) |
493 | return; |
494 | AstRangesVisitor ranges; |
495 | ranges.addItemRanges(item: rootItem.item(), itemLocations: rootItemLocations, currentP: Path()); |
496 | ranges.addNodeRanges(rootNode: n); |
497 | QStringView code = engine->code(); |
498 | QHash<AST::Node *, CommentedElement> & = ccomm->m_commentedElements; |
499 | quint32 = 0; |
500 | for (SourceLocation cLoc : engine->comments()) { |
501 | // collect whitespace before and after cLoc -> iPre..iPost contains whitespace, |
502 | // do not add newline before, but add the one after |
503 | quint32 iPre = cLoc.begin(); |
504 | int preNewline = 0; |
505 | int postNewline = 0; |
506 | QStringView ; |
507 | while (iPre > 0) { |
508 | QChar c = code.at(n: iPre - 1); |
509 | if (!c.isSpace()) { |
510 | if (commentStartStr.isEmpty() && (c == QLatin1Char('*') || c == QLatin1Char('/')) |
511 | && iPre - 1 > 0 && code.at(n: iPre - 2) == QLatin1Char('/')) { |
512 | commentStartStr = code.mid(pos: iPre - 2, n: 2); |
513 | --iPre; |
514 | } else { |
515 | break; |
516 | } |
517 | } else if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) { |
518 | preNewline = 1; |
519 | // possibly add an empty line if it was there (but never more than one) |
520 | int i = iPre - 1; |
521 | if (c == QLatin1Char('\n') && i > 0 && code.at(n: i - 1) == QLatin1Char('\r')) |
522 | --i; |
523 | while (i > 0 && code.at(n: --i).isSpace()) { |
524 | c = code.at(n: i); |
525 | if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) { |
526 | ++preNewline; |
527 | break; |
528 | } |
529 | } |
530 | break; |
531 | } |
532 | --iPre; |
533 | } |
534 | |
535 | if (iPre == 0) |
536 | preNewline = 1; |
537 | |
538 | qsizetype iPost = cLoc.end(); |
539 | while (iPost < code.size()) { |
540 | QChar c = code.at(n: iPost); |
541 | if (!c.isSpace()) { |
542 | if (!commentStartStr.isEmpty() && commentStartStr.at(n: 1) == QLatin1Char('*') |
543 | && c == QLatin1Char('*') && iPost + 1 < code.size() |
544 | && code.at(n: iPost + 1) == QLatin1Char('/')) { |
545 | commentStartStr = QStringView(); |
546 | ++iPost; |
547 | } else { |
548 | break; |
549 | } |
550 | } else { |
551 | if (c == QLatin1Char('\n')) { |
552 | ++postNewline; |
553 | if (iPost + 1 < code.size() && code.at(n: iPost + 1) == QLatin1Char('\n')) { |
554 | ++iPost; |
555 | ++postNewline; |
556 | } |
557 | } else if (c == QLatin1Char('\r')) { |
558 | if (iPost + 1 < code.size() && code.at(n: iPost + 1) == QLatin1Char('\n')) { |
559 | ++iPost; |
560 | ++postNewline; |
561 | } |
562 | } |
563 | } |
564 | ++iPost; |
565 | if (postNewline > 1) |
566 | break; |
567 | } |
568 | |
569 | ElementRef ; |
570 | bool pre = true; |
571 | auto iStart = ranges.starts.lowerBound(key: cLoc.begin()); |
572 | auto iEnd = ranges.ends.lowerBound(key: cLoc.begin()); |
573 | Q_ASSERT(!ranges.ends.isEmpty() && !ranges.starts.isEmpty()); |
574 | |
575 | auto checkElementBefore = [&]() { |
576 | if (commentEl) |
577 | return; |
578 | // prefer post comment attached to preceding element |
579 | auto preEnd = iEnd; |
580 | auto preStart = iStart; |
581 | if (preEnd != ranges.ends.begin()) { |
582 | --preEnd; |
583 | if (iStart == ranges.starts.begin() || (--preStart).key() < preEnd.key()) { |
584 | // iStart == begin should never happen |
585 | // check that we do not have operators (or in general other things) between |
586 | // preEnd and this because inserting a newline too ealy might invalidate the |
587 | // expression (think a + //comment\n b ==> a // comment\n + b), in this |
588 | // case attaching as preComment of iStart (b in the example) should be |
589 | // preferred as it is safe |
590 | quint32 i = iPre; |
591 | while (i != 0 && code.at(n: --i).isSpace()) |
592 | ; |
593 | if (i <= preEnd.key() || i < lastPostCommentPostEnd |
594 | || iEnd == ranges.ends.end()) { |
595 | commentEl = preEnd.value(); |
596 | pre = false; |
597 | lastPostCommentPostEnd = iPost + 1; // ensure the previous check works |
598 | // with multiple post comments |
599 | } |
600 | } |
601 | } |
602 | }; |
603 | auto checkElementAfter = [&]() { |
604 | if (commentEl) |
605 | return; |
606 | if (iStart != ranges.starts.end()) { |
607 | // try to add a pre comment of following element |
608 | if (iEnd == ranges.ends.end() || iEnd.key() > iStart.key()) { |
609 | // there is no end of element before iStart begins |
610 | // associate the comment as preComment of iStart |
611 | // (btw iEnd == end should never happen here) |
612 | commentEl = iStart.value(); |
613 | return; |
614 | } |
615 | } |
616 | if (iStart == ranges.starts.begin()) { |
617 | Q_ASSERT(iStart != ranges.starts.end()); |
618 | // we are before the first node (should be handled already by previous case) |
619 | commentEl = iStart.value(); |
620 | } |
621 | }; |
622 | auto checkInsideEl = [&]() { |
623 | if (commentEl) |
624 | return; |
625 | auto preIStart = iStart; |
626 | if (iStart == ranges.starts.begin()) { |
627 | commentEl = iStart.value(); // checkElementAfter should have handled this |
628 | return; |
629 | } else { |
630 | --preIStart; |
631 | } |
632 | // we are inside a node, actually inside both n1 and n2 (which might be the same) |
633 | // add to pre of the smallest between n1 and n2. |
634 | // This is needed because if there are multiple nodes starting/ending at the same |
635 | // place we store only the first (i.e. largest) |
636 | ElementRef n1 = preIStart.value(); |
637 | ElementRef n2 = iEnd.value(); |
638 | if (n1.size > n2.size) |
639 | commentEl = n2; |
640 | else |
641 | commentEl = n1; |
642 | }; |
643 | if (!preNewline) { |
644 | checkElementBefore(); |
645 | checkElementAfter(); |
646 | } else { |
647 | checkElementAfter(); |
648 | checkElementBefore(); |
649 | } |
650 | if (!commentEl) |
651 | checkInsideEl(); |
652 | if (!commentEl) { |
653 | qCWarning(commentsLog) << "Could not assign comment at" << locationToData(loc: cLoc) |
654 | << "adding before root node" ; |
655 | if (rootItem && (rootItemLocations || !n)) { |
656 | commentEl.element = RegionRef { .path: Path(), .regionName: QString() }; |
657 | commentEl.size = |
658 | rootItemLocations->info() |
659 | .regions.value(key: QString(), defaultValue: rootItemLocations->info().fullRegion) |
660 | .length; |
661 | // attach to rootItem |
662 | } else if (n) { |
663 | commentEl.element = n; |
664 | commentEl.size = n->lastSourceLocation().end() - n->firstSourceLocation().begin(); |
665 | } |
666 | } |
667 | |
668 | Comment (code.mid(pos: iPre, n: iPost - iPre), preNewline); |
669 | if (commentEl.element.index() == 0 && std::get<0>(v&: commentEl.element)) { |
670 | CommentedElement &cEl = commentedElements[std::get<0>(v&: commentEl.element)]; |
671 | if (pre) |
672 | cEl.preComments.append(t: comment); |
673 | else |
674 | cEl.postComments.append(t: comment); |
675 | } else if (commentEl.element.index() == 1) { |
676 | DomItem = rootItem.item() |
677 | .path(p: std::get<1>(v&: commentEl.element).path) |
678 | .field(name: Fields::comments); |
679 | if (RegionComments * = rComments.mutableAs<RegionComments>()) { |
680 | if (pre) |
681 | rCommentsPtr->addPreComment(comment, regionName: std::get<1>(v&: commentEl.element).regionName); |
682 | else |
683 | rCommentsPtr->addPostComment(comment, |
684 | regionName: std::get<1>(v&: commentEl.element).regionName); |
685 | } else { |
686 | Q_ASSERT(false); |
687 | } |
688 | } else { |
689 | qCWarning(commentsLog) |
690 | << "Failed: no item or node to attach comment" << comment.rawComment(); |
691 | } |
692 | } |
693 | } |
694 | |
695 | // internal class to collect all comments in a node or its subnodes |
696 | class : protected VisitAll |
697 | { |
698 | public: |
699 | (AstComments *, AST::Node *n) : comments(comments) |
700 | { |
701 | AST::Node::accept(node: n, visitor: this); |
702 | } |
703 | |
704 | void () override { } |
705 | |
706 | bool (Node *n) override |
707 | { |
708 | auto &cEls = comments->commentedElements(); |
709 | if (cEls.contains(key: n)) |
710 | nodeComments += cEls[n].commentGroups( |
711 | elLocation: combine(l1: n->firstSourceLocation(), l2: n->lastSourceLocation())); |
712 | return true; |
713 | } |
714 | |
715 | AstComments *; |
716 | QMultiMap<quint32, const QList<Comment> *> ; |
717 | }; |
718 | |
719 | /*! |
720 | \brief low level method returns all comments in a node (including its subnodes) |
721 | |
722 | The comments are roughly ordered in the order they appear in the file. |
723 | Multiple values are in reverse order if the index is even. |
724 | */ |
725 | QMultiMap<quint32, const QList<Comment> *> AstComments::(AST::Node *n) |
726 | { |
727 | CommentCollectorVisitor v(this, n); |
728 | return v.nodeComments; |
729 | } |
730 | |
731 | bool RegionComments::(DomItem &self, DirectVisitor visitor) |
732 | { |
733 | bool cont = true; |
734 | if (!regionComments.isEmpty()) |
735 | cont = cont && self.dvWrapField(visitor, f: Fields::regionComments, obj&: regionComments); |
736 | return cont; |
737 | } |
738 | |
739 | } // namespace Dom |
740 | } // namespace QQmlJS |
741 | QT_END_NAMESPACE |
742 | |