Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: 8767bee2eb2fa021a516418f372222e28b64aedc (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
/*
 * Copyright (c) 2013, 2015 QNX Software Systems and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.eclipse.cdt.internal.qt.ui.assist;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.dom.ast.DOMException;
import org.eclipse.cdt.core.dom.ast.IASTCompositeTypeSpecifier;
import org.eclipse.cdt.core.dom.ast.IASTEqualsInitializer;
import org.eclipse.cdt.core.dom.ast.IASTFileLocation;
import org.eclipse.cdt.core.dom.ast.IASTInitializer;
import org.eclipse.cdt.core.dom.ast.IASTInitializerClause;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.core.dom.ast.IASTNodeSelector;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.dom.ast.IBasicType;
import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.core.dom.ast.IType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPClassType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPFunctionType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPMethod;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPParameter;
import org.eclipse.cdt.core.index.IIndex;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.internal.core.dom.parser.cpp.CPPParameter;
import org.eclipse.cdt.internal.qt.core.index.IQMethod;
import org.eclipse.cdt.internal.qt.core.index.IQObject;
import org.eclipse.cdt.internal.qt.core.index.IQProperty;
import org.eclipse.cdt.internal.qt.core.index.QtIndex;
import org.eclipse.cdt.internal.qt.ui.Activator;
import org.eclipse.cdt.internal.ui.text.contentassist.CCompletionProposal;
import org.eclipse.cdt.ui.text.contentassist.ICEditorContentAssistInvocationContext;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.text.contentassist.ICompletionProposal;

/**
 * An attribute-based proposal depends on the both the attribute (the previous identifier) and the
 * containing class definition.  The class definition is not needed for all attribute types, but
 * is used to build the list of proposals for attributes like READ, WRITE, etc.
 */
@SuppressWarnings("restriction")
public class QPropertyAttributeProposal {
	private final int relevance;
	private final String identifier;
	private final String display;

	public QPropertyAttributeProposal(String identifier, int relevance) {
		this(identifier, identifier, relevance);
	}

	public ICompletionProposal createProposal(String prefix, int offset) {
		int prefixLen = prefix == null ? 0 : prefix.length();

		String disp = identifier.equals(display) ? display : (identifier + " - " + display);
		return new CCompletionProposal(identifier.substring(prefixLen), offset, prefixLen, Activator.getQtLogo(), disp,
				relevance);
	}

	private QPropertyAttributeProposal(String identifier, String display, int relevance) {
		this.identifier = identifier;
		this.display = display;
		this.relevance = relevance;
	}

	public String getIdentifier() {
		return identifier;
	}

	public static Collection<QPropertyAttributeProposal> buildProposals(IQProperty.Attribute attr,
			ICEditorContentAssistInvocationContext context, IType type, String name) {
		switch (attr) {
		// propose true/false for bool Attributes
		case DESIGNABLE:
		case SCRIPTABLE:
		case STORED:
		case USER:
			return Arrays.asList(new QPropertyAttributeProposal("true", IMethodAttribute.BaseRelevance + 11),
					new QPropertyAttributeProposal("false", IMethodAttribute.BaseRelevance + 10));

		// propose appropriate methods for method-based attributes
		case READ:
		case WRITE:
		case RESET:
			return getMethodProposals(context, get(attr, type, name));

		// propose appropriate signals for NOTIFY
		case NOTIFY:
			return getSignalProposals(context, get(attr, type, name));

		default:
			break;
		}

		return Collections.emptyList();
	}

	private static Collection<QPropertyAttributeProposal> getMethodProposals(
			ICEditorContentAssistInvocationContext context, IMethodAttribute methodAttribute) {

		ICPPClassType cls = getEnclosingClassDefinition(context);
		if (cls == null)
			return Collections.emptyList();

		// Return all the methods, including inherited and non-visible ones.
		ICPPMethod[] methods = cls.getMethods();
		List<ICPPMethod> filtered = new ArrayList<ICPPMethod>(methods.length);
		for (ICPPMethod method : methods)
			if (methodAttribute.keep(method))
				filtered.add(method);

		// TODO Choose the overload that is the best match -- closest parameter type and fewest
		//      parameters with default values.

		List<QPropertyAttributeProposal> proposals = new ArrayList<QPropertyAttributeProposal>();
		for (ICPPMethod method : getMethods(context, methodAttribute))
			proposals.add(new QPropertyAttributeProposal(method.getName(), getDisplay(cls, method),
					methodAttribute.getRelevance(method)));

		return proposals;
	}

	private static Collection<QPropertyAttributeProposal> getSignalProposals(
			ICEditorContentAssistInvocationContext context, IMethodAttribute methodAttribute) {
		ICPPClassType cls = getEnclosingClassDefinition(context);
		if (cls == null)
			return Collections.emptyList();

		ICProject cProject = context.getProject();
		if (cProject == null)
			return Collections.emptyList();

		QtIndex qtIndex = QtIndex.getIndex(cProject.getProject());
		if (qtIndex == null)
			return Collections.emptyList();

		IQObject qobj = null;
		try {
			qobj = qtIndex.findQObject(cls.getQualifiedName());
		} catch (DOMException e) {
			Activator.log(e);
		}

		if (qobj == null)
			return Collections.emptyList();

		List<QPropertyAttributeProposal> proposals = new ArrayList<QPropertyAttributeProposal>();
		for (IQMethod qMethod : qobj.getSignals().all())
			proposals.add(new QPropertyAttributeProposal(qMethod.getName(), IMethodAttribute.BaseRelevance));

		return proposals;
	}

	private static boolean isSameClass(ICPPClassType cls1, ICPPClassType cls2) {

		// IType.isSameType doesn't work in this case.  Given an instance of ICPPClassType, cls,
		// the following returns false:
		//     cls.isSameType( cls.getMethods()[0].getOwner() )
		//
		// Instead we check the fully qualified names.

		try {
			String[] qn1 = cls1.getQualifiedName();
			String[] qn2 = cls2.getQualifiedName();

			if (qn1.length != qn2.length)
				return false;

			for (int i = 0; i < qn1.length; ++i)
				if (!qn1[i].equals(qn2[i]))
					return false;
			return true;
		} catch (DOMException e) {
			return false;
		}
	}

	private static String getDisplay(ICPPClassType referenceContext, ICPPMethod method) {

		boolean includeClassname = !isSameClass(referenceContext, method.getClassOwner());

		StringBuilder sig = new StringBuilder();
		ICPPFunctionType type = method.getType();

		sig.append(type.getReturnType().toString());
		sig.append(' ');
		if (includeClassname) {
			sig.append(method.getOwner().getName());
			sig.append("::");
		}
		sig.append(method.getName());
		sig.append('(');
		boolean first = true;
		for (ICPPParameter param : method.getParameters()) {
			if (first)
				first = false;
			else
				sig.append(", ");

			String defValue = null;
			if (param instanceof CPPParameter) {
				CPPParameter cppParam = (CPPParameter) param;
				IASTInitializer defaultValue = cppParam.getInitializer();
				if (defaultValue instanceof IASTEqualsInitializer) {
					IASTInitializerClause clause = ((IASTEqualsInitializer) defaultValue).getInitializerClause();
					defValue = clause.toString();
				}
			}

			sig.append(defValue == null ? param.getType().toString() : defValue);
		}
		sig.append(')');
		return sig.toString();
	}

	private static interface IMethodAttribute {
		public boolean keep(ICPPMethod method);

		public static final int BaseRelevance = 2000;

		public int getRelevance(ICPPMethod method);

		public static final IMethodAttribute Null = new IMethodAttribute() {
			@Override
			public boolean keep(ICPPMethod method) {
				return false;
			}

			@Override
			public int getRelevance(ICPPMethod method) {
				return 0;
			}
		};
	}

	private static IMethodAttribute get(IQProperty.Attribute attr, IType type, String propertyName) {
		switch (attr) {
		case READ:
			return new Read(type, propertyName);
		case WRITE:
			return new Write(type, propertyName);
		case RESET:
			return new Reset(type, propertyName);
		default:
			return IMethodAttribute.Null;
		}
	}

	private static class Read implements IMethodAttribute {
		private final IType type;
		private final String propertyName;

		public Read(IType type, String propertyName) {
			this.type = type;
			this.propertyName = propertyName;
		}

		// From the Qt docs, http://qt-project.org/doc/qt-4.8/properties.html:
		// "A READ accessor function is required. It is for reading the property value. Ideally, a
		// const function is used for this purpose, and it must return either the property's type
		// or a pointer or reference to that type. e.g., QWidget::focus is a read-only property with
		// READ function, QWidget::hasFocus().
		@Override
		public boolean keep(ICPPMethod method) {
			// READ must have no params without default values
			if (method.getParameters().length > 0 && !method.getParameters()[0].hasDefaultValue())
				return false;

			// Make sure the return type of the method can be assigned to the property's type.
			IType retType = method.getType().getReturnType();
			if (!isAssignable(retType, type))
				return false;

			return true;
		}

		@Override
		public int getRelevance(ICPPMethod method) {
			String methodName = method.getName();
			if (methodName == null)
				return 0;

			// exact match is the most relevant
			if (methodName.equals(propertyName))
				return BaseRelevance + 20;

			// accessor with "get" prefix is the 2nd highest rank
			if (methodName.equalsIgnoreCase("get" + propertyName))
				return BaseRelevance + 19;

			// method names that include the property name anywhere are the next
			// most relevant
			if (methodName.matches(".*(?i)" + propertyName + ".*"))
				return BaseRelevance + 18;

			// otherwise return default relevance
			return 10;
		}
	}

	private static class Write implements IMethodAttribute {
		private final IType type;
		private final String propertyName;

		public Write(IType type, String propertyName) {
			this.type = type;
			this.propertyName = propertyName;
		}

		// From the Qt docs, http://qt-project.org/doc/qt-4.8/properties.html:
		// A WRITE accessor function is optional. It is for setting the property value. It must
		// return void and must take exactly one argument, either of the property's type or a
		// pointer or reference to that type. e.g., QWidget::enabled has the WRITE function
		// QWidget::setEnabled(). Read-only properties do not need WRITE functions. e.g., QWidget::focus
		// has no WRITE function.
		@Override
		public boolean keep(ICPPMethod method) {

			// The Qt moc doesn't seem to check that the return type is void, and I'm not sure why it
			// would need to.  This filter doesn't reject non-void methods.

			// WRITE must have at least one parameter and no more than one param without default values
			if (method.getParameters().length < 1
					|| (method.getParameters().length > 1 && !method.getParameters()[1].hasDefaultValue()))
				return false;

			// Make sure the property's type can be assigned to the type of the first parameter
			IType paramType = method.getParameters()[0].getType();
			if (!isAssignable(type, paramType))
				return false;

			return true;
		}

		@Override
		public int getRelevance(ICPPMethod method) {
			String methodName = method.getName();
			if (methodName == null)
				return 0;

			// exact match is the most relevant
			if (methodName.equals(propertyName))
				return BaseRelevance + 20;

			// accessor with "get" prefix is the 2nd highest rank
			if (methodName.equalsIgnoreCase("set" + propertyName))
				return BaseRelevance + 19;

			// method names that include the property name anywhere are the next
			// most relevant
			if (methodName.matches(".*(?i)" + propertyName + ".*"))
				return BaseRelevance + 18;

			// otherwise return default relevance
			return 10;
		}
	}

	private static class Reset implements IMethodAttribute {
		private final IType type;
		private final String propertyName;

		public Reset(IType type, String propertyName) {
			this.type = type;
			this.propertyName = propertyName;
		}

		// From the Qt docs, http://qt-project.org/doc/qt-4.8/properties.html:
		// A RESET function is optional. It is for setting the property back to its context
		// specific default value. e.g., QWidget::cursor has the typical READ and WRITE
		// functions, QWidget::cursor() and QWidget::setCursor(), and it also has a RESET
		// function, QWidget::unsetCursor(), since no call to QWidget::setCursor() can mean
		// reset to the context specific cursor. The RESET function must return void and take
		// no parameters.
		@Override
		public boolean keep(ICPPMethod method) {

			// RESET must have void return type
			IType retType = method.getType().getReturnType();
			if (!(retType instanceof IBasicType) || ((IBasicType) retType).getKind() != IBasicType.Kind.eVoid)
				return false;

			// RESET must have no parameters
			if (method.getParameters().length > 0)
				return false;

			return true;
		}

		@Override
		public int getRelevance(ICPPMethod method) {
			String methodName = method.getName();
			if (methodName == null)
				return 0;

			// accessor with "reet" prefix is the most relevant
			if (methodName.equalsIgnoreCase("reset" + propertyName))
				return BaseRelevance + 20;

			// method names that include the property name anywhere are the next
			// most relevant
			if (methodName.matches(".*(?i)" + propertyName + ".*"))
				return BaseRelevance + 18;

			// otherwise return default relevance
			return 10;
		}
	}

	private static ICPPClassType getEnclosingClassDefinition(ICEditorContentAssistInvocationContext context) {
		try {
			IIndex index = CCorePlugin.getIndexManager().getIndex(context.getProject());
			ITranslationUnit tu = context.getTranslationUnit();
			if (tu == null)
				return null;

			// Disable all unneeded parts of the parser.
			IASTTranslationUnit astTU = tu.getAST(index,
					ITranslationUnit.AST_SKIP_FUNCTION_BODIES | ITranslationUnit.AST_SKIP_ALL_HEADERS
							| ITranslationUnit.AST_CONFIGURE_USING_SOURCE_CONTEXT
							| ITranslationUnit.AST_SKIP_TRIVIAL_EXPRESSIONS_IN_AGGREGATE_INITIALIZERS
							| ITranslationUnit.AST_PARSE_INACTIVE_CODE);
			if (astTU == null)
				return null;

			IASTNodeSelector selector = astTU.getNodeSelector(null);

			// Macro expansions don't provide valid enclosing nodes.  Backup until we are no longer in a
			// macro expansions.  A loop is needed because consecutive expansions have no valid node
			// between them.
			int offset = context.getInvocationOffset();
			IASTNode enclosing;
			do {
				enclosing = selector.findEnclosingNode(offset, 0);
				if (enclosing == null)
					return null;

				IASTFileLocation location = enclosing.getFileLocation();
				if (location == null)
					return null;

				offset = location.getNodeOffset() - 1;
			} while (offset > 0 && !(enclosing instanceof IASTCompositeTypeSpecifier));

			if (!(enclosing instanceof IASTCompositeTypeSpecifier))
				return null;

			IASTName name = ((IASTCompositeTypeSpecifier) enclosing).getName();
			if (name == null)
				return null;

			IBinding binding = name.getBinding();
			if (binding == null)
				return null;

			return (ICPPClassType) binding.getAdapter(ICPPClassType.class);
		} catch (CoreException e) {
			Activator.log(e);
		}

		return null;
	}

	/**
	 * Find and return all methods that are accessible in the class definition that encloses the argument
	 * invocation context.  Does not return null.
	 */
	private static Collection<ICPPMethod> getMethods(ICEditorContentAssistInvocationContext context,
			IMethodAttribute methodAttribute) {

		ICPPClassType cls = getEnclosingClassDefinition(context);
		if (cls == null)
			return Collections.emptyList();

		// Return all the methods, including inherited and non-visible ones.
		ICPPMethod[] methods = cls.getMethods();
		List<ICPPMethod> filtered = new ArrayList<ICPPMethod>(methods.length);
		for (ICPPMethod method : methods)
			if (methodAttribute.keep(method))
				filtered.add(method);

		// TODO Choose the overload that is the best match -- closest parameter type and fewest
		//      parameters with default values.

		return filtered;
	}

	private static boolean isAssignable(IType lhs, IType rhs) {
		// TODO This needs a real assignment check.  If the types are different by implicitly convertible
		//      then this should return true.
		return lhs != null && rhs.isSameType(lhs);
	}
}

Back to the top