/*
* Copyright (c) 2013, 2015 QNX Software Systems and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.cdt.internal.qt.core.pdom;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.cdt.core.dom.ast.IASTDeclaration;
import org.eclipse.cdt.core.dom.ast.IASTFileLocation;
import org.eclipse.cdt.core.dom.ast.IASTMacroExpansionLocation;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTNodeLocation;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorMacroExpansion;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTCompositeTypeSpecifier;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTVisibilityLabel;
import org.eclipse.cdt.internal.qt.core.QtKeywords;
import org.eclipse.cdt.internal.qt.core.index.IQMethod;
/**
* The AST for a QObject is separated into regions based on macro expansions. These
* regions determine the Qt kind for methods that are declared within them.
* <p>
* This utility class makes one pass over the C++ class specification to identify
* all such regions. It also provides an iterator that can be used while examining
* the class spec's members.
*/
public class QtASTClass {
private final Iterator<Region> regions;
private final Iterator<Tag> tags;
private final Iterator<Revision> revisions;
private Region region;
private Tag tag;
private Revision revision;
/**
* Must only be called with increasing offset. Internal pointers may be advanced on
* each call.
*/
public IQMethod.Kind getKindFor(int offset) {
// There are 3 steps:
// 1) The tags counter must always be advanced. Tags only apply to the next declaration
// and therefore the internal counter must always be advanced. Multiple tags are
// collapsed to find the highest precedence value.
// 2) The region counter is advanced to find a region that either contains the offset
// or is the first region after the offset. Regions override tags, so we use the
// region kind if one is found.
// 3) The final result is based on tags (if they were present).
//
// This precedence is based on experimentation with the moc (ver 63). It
// ignores macros tagging a single method when that method is declared within
// a signal/slot region. E.g., the following example has two signals and one slot:
//
// class Q : public QObject
// {
// Q_OBJECT
// signals: void signal1();
// Q_SLOT void signal2(); /* Tagged with Q_SLOT, but the declaration is within the
// * signals region, so the moc considers it a signal. */
// public:
// Q_SLOT void slot1();
// };
// Consume all tags since the last declaration to find the highest precedence tag.
IQMethod.Kind kind = IQMethod.Kind.Unspecified;
while(tag != null && tag.offset < offset) {
kind = getHigherPrecedence(kind, tag.kind);
tag = tags.hasNext() ? tags.next() : null;
}
// Advance regions to find one that does not end before this offset.
while(region != null && region.end < offset)
region = regions.hasNext() ? regions.next() : null;
// If the offset is within this region, then use its kind.
if (region != null && region.contains(offset))
kind = region.kind;
return kind;
}
/**
* Must only be called with increasing offset. Internal pointers may be advanced on
* each call.
*/
public Long getRevisionFor(int offset) {
// Consume all revisions since the last declaration to find one (if any) that applies
// to this declaration.
Long rev = null;
while(revision != null && revision.offset < offset) {
rev = revision.revision;
revision = revisions.hasNext() ? revisions.next() : null;
}
return rev;
}
private static IQMethod.Kind getHigherPrecedence(IQMethod.Kind kind1, IQMethod.Kind kind2) {
switch(kind1) {
case Unspecified:
return kind2;
case Invokable:
switch(kind2) {
case Slot:
case Signal:
return kind2;
default:
return kind1;
}
case Signal:
if (kind2 == IQMethod.Kind.Slot)
return kind2;
return kind2;
case Slot:
return kind1;
}
return IQMethod.Kind.Unspecified;
}
public static QtASTClass create(ICPPASTCompositeTypeSpecifier spec) {
// There is more detail in Bug 401696 describing why this needs to look at all
// the node locations. Briefly, the CDT parser does not associate empty macros
// with the function when they are the first thing in the declaration. E.g.,
//
// #define X
// void func1() {}
// X void func2() {}
//
// Could also look like:
// void func1() {} X
// void func2() {}
//
// The nodes are processed in three stages which are described in detail below. Only
// the first stage looks at the nodes, the later stages just cleanup results from the
// first walk over all node locations.
// 1) Examine the locations to find all macro expansions. This finds a beginning and
// highest possible end for the regions. It also locates the offset for single-method
// tags (including resolving precedence).
// This allows single-method tags to overlap regions because regions may be shortened
// by a later step.
ArrayList<Tag> tags = new ArrayList<Tag>();
ArrayList<Revision> revisions = new ArrayList<Revision>();
ArrayList<Region> regions = new ArrayList<Region>();
Region currRegion = null;
for(IASTNodeLocation location : spec.getNodeLocations()) {
Tag tag = Tag.create(location);
if (tag != null)
tags.add(tag);
Revision revision = Revision.create(location);
if (revision != null)
revisions.add(revision);
Region region = Region.create(location);
if (region != null) {
if (currRegion != null)
currRegion.end = region.begin;
currRegion = region;
regions.add(region);
}
}
// 2) Make the regions smaller where visibility labels are introduced.
if (!regions.isEmpty()) {
Iterator<Region> iterator = regions.iterator();
Region region = iterator.next();
for (IASTDeclaration decl : spec.getMembers()) {
// Ignore everything other than visibility labels.
if (!(decl instanceof ICPPASTVisibilityLabel))
continue;
int offset = decl.getFileLocation().getNodeOffset();
// Otherwise terminate all regions that start before this label and advance
// to the first one that follows.
while(region != null && region.begin < offset) {
region.end = offset;
region = iterator.hasNext() ? iterator.next() : null;
}
// Stop searching for visibility labels after the last region has been terminated.
if (region == null)
break;
}
}
// 3) Eliminate tags that are within regions.
if (!tags.isEmpty()) {
Iterator<Tag> iterator = tags.iterator();
Tag tag = iterator.next();
for(Region region : regions) {
// Keep all tags that are before the start of this region.
while(tag != null && tag.offset < region.begin)
tag = iterator.hasNext() ? iterator.next() : null;
// Delete all tags that are within this region.
while(tag != null && region.contains(tag.offset)) {
iterator.remove();
tag = iterator.hasNext() ? iterator.next() : null;
}
// Stop searching when there are no more tags to be examined.
if (tag == null)
break;
}
}
return new QtASTClass(regions, tags, revisions);
}
private QtASTClass(List<Region> regions, List<Tag> tags, List<Revision> revisions) {
this.regions = regions.iterator();
this.tags = tags.iterator();
this.revisions = revisions.iterator();
this.region = this.regions.hasNext() ? this.regions.next() : null;
this.tag = this.tags.hasNext() ? this.tags.next() : null;
this.revision = this.revisions.hasNext() ? this.revisions.next() : null;
}
private static class Region {
public final int begin;
public int end = Integer.MAX_VALUE;
public final IQMethod.Kind kind;
public Region(int begin, IQMethod.Kind kind) {
this.begin = begin;
this.kind = kind;
}
public boolean contains(int offset) {
return offset >= begin
&& offset < end;
}
/**
* Return a region for the given location or null if the location does not
* introduce a region.
*/
public static Region create(IASTNodeLocation location) {
if (!(location instanceof IASTMacroExpansionLocation))
return null;
IASTMacroExpansionLocation macroLocation = (IASTMacroExpansionLocation) location;
IASTFileLocation fileLocation = macroLocation.asFileLocation();
if (fileLocation == null)
return null;
int offset = fileLocation.getNodeOffset();
IASTPreprocessorMacroExpansion expansion = macroLocation.getExpansion();
String macroName = getMacroName(expansion);
if (QtKeywords.Q_SLOTS.equals(macroName)
|| QtKeywords.SLOTS.equals(macroName))
return new Region(offset, IQMethod.Kind.Slot);
if (QtKeywords.Q_SIGNALS.equals(macroName)
|| QtKeywords.SIGNALS.equals(macroName))
return new Region(offset, IQMethod.Kind.Signal);
return null;
}
}
private static class Tag {
public final int offset;
public IQMethod.Kind kind;
private Tag(int begin, IQMethod.Kind kind) {
this.offset = begin;
this.kind = kind;
}
/**
* Return a tag for the given location or null if the location does not
* introduce a tag.
*/
public static Tag create(IASTNodeLocation location) {
if (!(location instanceof IASTMacroExpansionLocation))
return null;
IASTMacroExpansionLocation macroLocation = (IASTMacroExpansionLocation) location;
IASTFileLocation fileLocation = macroLocation.asFileLocation();
if (fileLocation == null)
return null;
int offset = fileLocation.getNodeOffset();
IASTPreprocessorMacroExpansion expansion = macroLocation.getExpansion();
String macroName = getMacroName(expansion);
if (QtKeywords.Q_SLOT.equals(macroName))
return new Tag(offset, IQMethod.Kind.Slot);
if (QtKeywords.Q_SIGNAL.equals(macroName))
return new Tag(offset, IQMethod.Kind.Signal);
if (QtKeywords.Q_INVOKABLE.equals(macroName))
return new Tag(offset, IQMethod.Kind.Invokable);
return null;
}
}
private static class Revision {
private final int offset;
private final Long revision;
// This regular expression matches Q_REVISION macro expansions. It allows C++ integer
// literals as the expansion parameter. The integer literal is provided in capture
// group 1. Hexadecimal and octal prefixes are included in the capture group. Unsigned
// and long suffixes are allowed but are excluded from the capture group. The matcher's
// input string should be trimmed and have all newlines replaced.
private static final Pattern QREVISION_REGEX = Pattern.compile("^Q_REVISION\\s*\\(\\s*((?:0x)?[\\da-fA-F]+)[ulUL]*\\s*\\)$");
public Revision(int offset, Long revision) {
this.offset = offset;
this.revision = revision;
}
/**
* Return a tag for the given location or null if the location does not
* introduce a tag.
*/
public static Revision create(IASTNodeLocation location) {
if (!(location instanceof IASTMacroExpansionLocation))
return null;
IASTMacroExpansionLocation macroLocation = (IASTMacroExpansionLocation) location;
IASTFileLocation fileLocation = macroLocation.asFileLocation();
if (fileLocation == null)
return null;
int offset = fileLocation.getNodeOffset();
IASTPreprocessorMacroExpansion expansion = macroLocation.getExpansion();
String macroName = getMacroName(expansion);
if (!QtKeywords.Q_REVISION.equals(macroName))
return null;
String raw = expansion.getRawSignature();
if (raw == null)
return null;
// Trim leading and trailing whitespace and remove all newlines.
Matcher m = QREVISION_REGEX.matcher(raw.trim().replaceAll("\\s+", ""));
if (m.matches()) {
try {
return new Revision(offset, Long.parseLong(m.group(1)));
} catch(NumberFormatException e) {
// The number will be parsed incorrectly when the C++ client code does not
// contain a valid integer. We can't do anything about that, so the exception
// is ignored. A codan checker could notify the user of this problem.
}
}
return null;
}
}
/**
* Find and return the simple name of the macro that is being expanded or null if the name
* cannot be found.
*/
private static String getMacroName(IASTPreprocessorMacroExpansion expansion) {
if (expansion == null)
return null;
IASTName name = expansion.getMacroReference();
return name == null ? null : name.toString();
}
}