1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "qmljscompletioncontextfinder.h"
27 #include "qmljsscanner.h"
28 
29 #include <utils/porting.h>
30 
31 #include <QTextDocument>
32 #include <QStringList>
33 
34 using namespace QmlJS;
35 
36 /*
37     Saves and restores the state of the global linizer. This enables
38     backtracking.
39 
40     Identical to the defines in qmljslineinfo.cpp
41 */
42 #define YY_SAVE() LinizerState savedState = yyLinizerState
43 #define YY_RESTORE() yyLinizerState = savedState
44 
CompletionContextFinder(const QTextCursor & cursor)45 CompletionContextFinder::CompletionContextFinder(const QTextCursor &cursor)
46     : m_cursor(cursor)
47     , m_colonCount(-1)
48     , m_behaviorBinding(false)
49     , m_inStringLiteral(false)
50     , m_inImport(false)
51 {
52     QTextBlock lastBlock = cursor.block();
53     if (lastBlock.next().isValid())
54         lastBlock = lastBlock.next();
55     initialize(cursor.document()->begin(), lastBlock);
56 
57     m_startTokenIndex = yyLinizerState.tokens.size() - 1;
58 
59     // Initialize calls readLine - which skips empty lines. We should only adjust
60     // the start token index if the linizer still is in the same block as the cursor.
61     const int cursorPos = cursor.positionInBlock();
62     if (yyLinizerState.iter == cursor.block()) {
63         for (; m_startTokenIndex >= 0; --m_startTokenIndex) {
64             const Token &token = yyLinizerState.tokens.at(m_startTokenIndex);
65             if (token.end() <= cursorPos)
66                 break;
67             if (token.begin() < cursorPos && token.is(Token::String))
68                 m_inStringLiteral = true;
69         }
70 
71         if (m_startTokenIndex == yyLinizerState.tokens.size() - 1 && yyLinizerState.insertedSemicolon)
72             --m_startTokenIndex;
73     }
74 
75     getQmlObjectTypeName(m_startTokenIndex);
76     checkBinding();
77     checkImport();
78 }
79 
getQmlObjectTypeName(int startTokenIndex)80 void CompletionContextFinder::getQmlObjectTypeName(int startTokenIndex)
81 {
82     YY_SAVE();
83 
84     int tokenIndex = findOpeningBrace(startTokenIndex);
85     if (tokenIndex != -1) {
86         --tokenIndex;
87 
88         // can be one of
89         // A.B on c.d
90         // A.B
91 
92         bool identifierExpected = true;
93         int i = tokenIndex;
94         forever {
95             if (i < 0) {
96                 if (!readLine())
97                     break;
98                 else
99                     i = yyLinizerState.tokens.size() - 1;
100             }
101 
102             const Token &token = yyLinizerState.tokens.at(i);
103             if (!identifierExpected && token.kind == Token::Dot) {
104                 identifierExpected = true;
105             } else if (token.kind == Token::Identifier) {
106                 const QString idText = yyLine->mid(token.begin(), token.length);
107                 if (identifierExpected) {
108                     m_qmlObjectTypeName.prepend(idText);
109                     identifierExpected = false;
110                 } else if (idText == QLatin1String("on")) {
111                     m_qmlObjectTypeName.clear();
112                     identifierExpected = true;
113                 } else {
114                     break;
115                 }
116             } else {
117                 break;
118             }
119 
120             --i;
121         }
122     }
123 
124     YY_RESTORE();
125 }
126 
checkBinding()127 void CompletionContextFinder::checkBinding()
128 {
129     YY_SAVE();
130 
131     //qCDebug(qmljsLog) << "Start line:" << *yyLine << m_startTokenIndex;
132 
133     int i = m_startTokenIndex;
134     int colonCount = 0;
135     bool delimiterFound = false;
136     bool identifierExpected = false;
137     bool dotExpected = false;
138     while (!delimiterFound) {
139         if (i < 0) {
140             if (!readLine())
141                 break;
142             else
143                 i = yyLinizerState.tokens.size() - 1;
144             //qDebug() << "New Line" << *yyLine;
145         }
146 
147         const Token &token = yyLinizerState.tokens.at(i);
148         //qDebug() << "Token:" << yyLine->mid(token.begin(), token.length);
149 
150         switch (token.kind) {
151         case Token::RightBrace:
152         case Token::LeftBrace:
153         case Token::Semicolon:
154             delimiterFound = true;
155             break;
156 
157         case Token::Colon:
158             ++colonCount;
159             identifierExpected = true;
160             dotExpected = false;
161             m_behaviorBinding = false;
162             m_bindingPropertyName.clear();
163             break;
164 
165         case Token::Identifier: {
166             const QStringView tokenString = Utils::midView(*yyLine, token.begin(), token.length);
167             dotExpected = false;
168             if (identifierExpected) {
169                 m_bindingPropertyName.prepend(tokenString.toString());
170                 identifierExpected = false;
171                 dotExpected = true;
172             } else if (tokenString == QLatin1String("on")) {
173                 m_behaviorBinding = true;
174             }
175         } break;
176 
177         case Token::Dot:
178             if (dotExpected) {
179                 dotExpected = false;
180                 identifierExpected = true;
181             } else {
182                 identifierExpected = false;
183             }
184             break;
185 
186         default:
187             dotExpected = false;
188             identifierExpected = false;
189             break;
190         }
191 
192         --i;
193     }
194 
195     YY_RESTORE();
196     if (delimiterFound)
197         m_colonCount = colonCount;
198 }
199 
checkImport()200 void CompletionContextFinder::checkImport()
201 {
202     YY_SAVE();
203 
204     //qDebug() << "Start line:" << *yyLine << m_startTokenIndex;
205 
206     QStringList libVersionImport;
207     int isInLibVersionImport;
208     int i = m_startTokenIndex;
209     bool stop = false;
210     enum State {
211         Unknown,
212         ExpectImport =              1 << 0,
213         ExpectTargetDot =           1 << 1,
214         ExpectTargetIdentifier =    1 << 2,
215         ExpectAnyTarget =           1 << 3,
216         ExpectVersion =             1 << 4,
217         ExpectAs =                  1 << 5
218     };
219     State state = Unknown;
220     isInLibVersionImport = -1;
221 
222     while (!stop) {
223         if (i < 0) {
224             if (!readLine())
225                 break;
226             else
227                 i = yyLinizerState.tokens.size() - 1;
228             //qDebug() << "New Line" << *yyLine;
229         }
230 
231         const Token &token = yyLinizerState.tokens.at(i);
232         //qDebug() << "Token:" << yyLine->mid(token.begin(), token.length);
233 
234         switch (token.kind) {
235         case Token::Identifier: {
236             const QStringView tokenString = Utils::midView(*yyLine, token.begin(), token.length);
237             if (tokenString == QLatin1String("as")) {
238                 isInLibVersionImport = 0;
239                 if (state == Unknown) {
240                     state = State(ExpectAnyTarget | ExpectVersion);
241                     break;
242                 }
243             } else if (tokenString == QLatin1String("import")) {
244                 if (state == Unknown || (state & ExpectImport)) {
245                     if (isInLibVersionImport == -1 && token.end() < m_cursor.position())
246                         isInLibVersionImport = 1;
247                     m_inImport = true;
248                 }
249             } else {
250                 if (state == Unknown || (state & ExpectAnyTarget)
251                         || (state & ExpectTargetIdentifier)) {
252                     state = State(ExpectImport | ExpectTargetDot);
253                     libVersionImport.prepend(tokenString.toString());
254                     if (isInLibVersionImport == -1) {
255                         if (token.end() < m_cursor.position())
256                             libVersionImport.append(QLatin1String(" "));
257                         isInLibVersionImport = 1;
258                     }
259                     break;
260                 }
261             }
262             stop = true;
263             break;
264         }
265         case Token::String:
266             if (state == Unknown || (state & ExpectAnyTarget)) {
267                 state = ExpectImport;
268                 break;
269             }
270             stop = true;
271             break;
272         case Token::Number:
273             if (state == Unknown || (state & ExpectVersion)) {
274                 state = ExpectAnyTarget;
275                 libVersionImport.prepend(yyLine->mid(token.begin(), token.length));
276                 libVersionImport.prepend(QLatin1String(" "));
277                 if (isInLibVersionImport == -1)
278                     isInLibVersionImport = 1;
279                 break;
280             }
281             stop = true;
282             break;
283         case Token::Dot:
284             if (state == Unknown || (state & ExpectTargetDot)) {
285                 state = ExpectTargetIdentifier;
286                 libVersionImport.prepend(QLatin1String("."));
287                 if (isInLibVersionImport == -1)
288                     isInLibVersionImport = 1;
289                 break;
290             }
291             stop = true;
292             break;
293 
294         default:
295             stop = true;
296             break;
297         }
298         if (isInLibVersionImport == -1)
299             isInLibVersionImport = 0;
300         --i;
301     }
302 
303     YY_RESTORE();
304     if (m_inImport && isInLibVersionImport == 1) {
305         m_libVersion = libVersionImport.join(QString());
306         if (m_libVersion.isNull())
307             m_libVersion = QLatin1String("");
308     } else {
309         m_libVersion.clear();
310     }
311 }
312 
qmlObjectTypeName() const313 QStringList CompletionContextFinder::qmlObjectTypeName() const
314 {
315     return m_qmlObjectTypeName;
316 }
317 
isInQmlContext() const318 bool CompletionContextFinder::isInQmlContext() const
319 {
320     return !qmlObjectTypeName().isEmpty();
321 }
322 
isInLhsOfBinding() const323 bool CompletionContextFinder::isInLhsOfBinding() const
324 {
325     return isInQmlContext() && m_colonCount == 0;
326 }
327 
isInRhsOfBinding() const328 bool CompletionContextFinder::isInRhsOfBinding() const
329 {
330     return isInQmlContext() && m_colonCount >= 1;
331 }
332 
bindingPropertyName() const333 QStringList CompletionContextFinder::bindingPropertyName() const
334 {
335     return m_bindingPropertyName;
336 }
337 
338 /*!
339   \return true if the cursor after "Type on" in the left hand side
340   of a binding, false otherwise.
341 */
isAfterOnInLhsOfBinding() const342 bool CompletionContextFinder::isAfterOnInLhsOfBinding() const
343 {
344     return isInLhsOfBinding() && m_behaviorBinding;
345 }
346 
isInStringLiteral() const347 bool QmlJS::CompletionContextFinder::isInStringLiteral() const
348 {
349     return m_inStringLiteral;
350 }
351 
isInImport() const352 bool QmlJS::CompletionContextFinder::isInImport() const
353 {
354     return m_inImport;
355 }
356 
libVersionImport() const357 QString CompletionContextFinder::libVersionImport() const
358 {
359     return m_libVersion;
360 }
361 
findOpeningBrace(int startTokenIndex)362 int CompletionContextFinder::findOpeningBrace(int startTokenIndex)
363 {
364     YY_SAVE();
365 
366     if (startTokenIndex == -1)
367         readLine();
368 
369     Token::Kind nestedClosing = Token::EndOfFile;
370     int nestingCount = 0;
371 
372     for (int i = 0; i < BigRoof; ++i) {
373         if (i != 0 || startTokenIndex == -1)
374             startTokenIndex = yyLinizerState.tokens.size() - 1;
375 
376         for (int t = startTokenIndex; t >= 0; --t) {
377             const Token &token = yyLinizerState.tokens.at(t);
378             switch (token.kind) {
379             case Token::LeftBrace:
380                 if (nestingCount > 0) {
381                     if (nestedClosing == Token::RightBrace)
382                         --nestingCount;
383                 } else {
384                     return t;
385                 }
386                 break;
387             case Token::LeftParenthesis:
388                 if (nestingCount > 0) {
389                     if (nestedClosing == Token::RightParenthesis)
390                         --nestingCount;
391                 } else {
392                     YY_RESTORE();
393                     return -1;
394                 }
395                 break;
396             case Token::LeftBracket:
397                 if (nestingCount > 0) {
398                     if (nestedClosing == Token::RightBracket)
399                         --nestingCount;
400                 } else {
401                     YY_RESTORE();
402                     return -1;
403                 }
404                 break;
405 
406             case Token::RightBrace:
407             case Token::RightParenthesis:
408             case Token::RightBracket:
409                 if (nestingCount == 0) {
410                     nestedClosing = token.kind;
411                     nestingCount = 1;
412                 } else if (nestedClosing == token.kind) {
413                     ++nestingCount;
414                 }
415                 break;
416 
417             default: break;
418             }
419         }
420 
421         if (! readLine())
422             break;
423     }
424 
425     YY_RESTORE();
426     return -1;
427 }
428