blob: 497efdcc0fa6e26b3b90dc2636974fde54a99adf [file] [log] [blame]
/*
* Copyright 2017 The Bazel Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.idea.blaze.cpp.includes;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiComment;
import com.jetbrains.cidr.lang.editor.OCCommenter;
import com.jetbrains.cidr.lang.psi.OCFile;
import com.jetbrains.cidr.lang.psi.OCIncludeDirective;
import com.jetbrains.cidr.lang.psi.visitors.OCRecursiveVisitor;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Parses IWYU pragmas specified in a given file.
*
* <p>See:
* https://github.com/include-what-you-use/include-what-you-use/blob/master/docs/IWYUPragmas.md
*/
final class IwyuPragmas {
private static final String IWYU_PREFIX = "IWYU pragma:";
public final OCFile file;
public final Optional<PrivatePragma> privatePragma;
public final ImmutableSet<KeepPragma> keeps;
public final ImmutableSet<ExportPragma> exports;
public final Optional<IncludePath> associatedHeader;
private IwyuPragmas(
OCFile file,
Optional<PrivatePragma> privatePragma,
ImmutableSet<KeepPragma> keeps,
ImmutableSet<ExportPragma> exports,
Optional<IncludePath> associatedHeader) {
this.file = file;
this.privatePragma = privatePragma;
this.keeps = keeps;
this.exports = exports;
this.associatedHeader = associatedHeader;
}
public static IwyuPragmas parse(OCFile file) {
BuildingVisitor builder = new BuildingVisitor(file);
file.accept(builder);
return builder.build();
}
/** Parse pragmas that are trailing comments */
interface TrailingPragmaParser {
/**
* Checks if the pragmaContent is parsed by this parser, and updates the builder state if it is
* actually parsed.
*
* @param builder builder to update on success
* @param directive include directive with a trailing pragma comment
* @param pragmaContent content from the pragma comment to parse
* @return true if handled by this parser
*/
boolean tryParse(BuildingVisitor builder, OCIncludeDirective directive, String pragmaContent);
}
/** Parse pragmas are standalone comments */
interface StandalonePragmaParser {
/**
* Checks if the pragmaContent is parsed by this parser, and updates the builder state if it is
* actually parsed.
*
* @param builder builder to update on success
* @param pragmaContent content from the pragma comment to parse
* @return true if handled by this parser
*/
boolean tryParse(BuildingVisitor builder, String pragmaContent);
}
/** Represents a "keep" pragma */
@AutoValue
public abstract static class KeepPragma {
/* TODO: keep pragmas can also be attached to a forward include. We don't yet handle that */
private static final TrailingPragmaParser PARSER = new Parser();
abstract IncludePath includePath();
static KeepPragma create(IncludePath includePath) {
return new AutoValue_IwyuPragmas_KeepPragma(includePath);
}
private static class Parser implements TrailingPragmaParser {
private static final Pattern KEEP_PATTERN = Pattern.compile("^\\s*keep\\s*$");
@Override
public boolean tryParse(
BuildingVisitor builder, OCIncludeDirective directive, String pragmaContent) {
Matcher matcher = KEEP_PATTERN.matcher(pragmaContent);
if (!matcher.find()) {
return false;
}
builder.keeps.add(
KeepPragma.create(
IncludePath.create(directive.getReferenceText(), directive.getDelimiters())));
return true;
}
}
}
/** Represents an "export" pragma */
@AutoValue
public abstract static class ExportPragma {
private static final TrailingPragmaParser TRAIL_PARSER = new TrailParser();
private static final StandalonePragmaParser RANGE_PARSER = new RangeParser();
abstract IncludePath includePath();
static ExportPragma create(IncludePath includePath) {
return new AutoValue_IwyuPragmas_ExportPragma(includePath);
}
private static class TrailParser implements TrailingPragmaParser {
private static final Pattern EXPORT_PATTERN = Pattern.compile("^\\s*export\\s*$");
@Override
public boolean tryParse(
BuildingVisitor builder, OCIncludeDirective directive, String pragmaContent) {
Matcher matcher = EXPORT_PATTERN.matcher(pragmaContent);
if (!matcher.find()) {
return false;
}
builder.exports.add(
ExportPragma.create(
IncludePath.create(directive.getReferenceText(), directive.getDelimiters())));
return true;
}
}
private static class RangeParser implements StandalonePragmaParser {
private static final Pattern BEGIN_PATTERN = Pattern.compile("^\\s*begin_exports\\s*$");
private static final Pattern END_PATTERN = Pattern.compile("^\\s*end_exports\\s*$");
@Override
public boolean tryParse(BuildingVisitor builder, String pragmaContent) {
Matcher matcher = BEGIN_PATTERN.matcher(pragmaContent);
if (matcher.matches()) {
builder.includesInRange.clear();
builder.collectRange = true;
return true;
}
matcher = END_PATTERN.matcher(pragmaContent);
if (matcher.matches()) {
builder.collectRange = false;
for (OCIncludeDirective directive : builder.includesInRange) {
builder.exports.add(
ExportPragma.create(
IncludePath.create(directive.getReferenceText(), directive.getDelimiters())));
}
builder.includesInRange.clear();
return true;
}
return false;
}
}
}
/** Represents the "private" pragma */
public static class PrivatePragma {
private static final StandalonePragmaParser PARSER = new Parser();
public final Optional<IncludePath> includeOther;
PrivatePragma(IncludePath includeOther) {
this.includeOther = Optional.of(includeOther);
}
PrivatePragma() {
this.includeOther = Optional.empty();
}
private static class Parser implements StandalonePragmaParser {
private static final Pattern PRIVATE_PATTERN =
Pattern.compile("^\\s*private\\s*(,\\s*include\\s*(?<includename>.*)\\s*)?$");
@Override
public boolean tryParse(BuildingVisitor builder, String pragmaContent) {
Matcher matcher = PRIVATE_PATTERN.matcher(pragmaContent);
if (!matcher.find()) {
return false;
}
String alternateInclude = matcher.group("includename");
if (alternateInclude != null) {
builder.privatePragma =
Optional.of(new PrivatePragma(IncludePath.create(alternateInclude)));
} else {
builder.privatePragma = Optional.of(new PrivatePragma());
}
return true;
}
}
}
/** Represents the "associated" pragma */
public static class AssociatedPragma {
private static final TrailingPragmaParser PARSER = new Parser();
private static class Parser implements TrailingPragmaParser {
private static final Pattern ASSOCIATED_PATTERN = Pattern.compile("^\\s*associated\\s*$");
@Override
public boolean tryParse(
BuildingVisitor builder, OCIncludeDirective directive, String pragmaContent) {
Matcher matcher = ASSOCIATED_PATTERN.matcher(pragmaContent);
if (!matcher.find()) {
return false;
}
builder.associatedHeader =
Optional.of(
IncludePath.create(directive.getReferenceText(), directive.getDelimiters()));
return true;
}
}
}
private static final ImmutableList<TrailingPragmaParser> TRAILING_PARSERS =
ImmutableList.of(KeepPragma.PARSER, ExportPragma.TRAIL_PARSER, AssociatedPragma.PARSER);
private static final ImmutableList<StandalonePragmaParser> STANDALONE_PARSERS =
ImmutableList.of(ExportPragma.RANGE_PARSER, PrivatePragma.PARSER);
private static class BuildingVisitor extends OCRecursiveVisitor {
final OCFile file;
final OCCommenter commenter;
Optional<PrivatePragma> privatePragma = Optional.empty();
ImmutableSet.Builder<KeepPragma> keeps = ImmutableSet.builder();
ImmutableSet.Builder<ExportPragma> exports = ImmutableSet.builder();
Optional<IncludePath> associatedHeader = Optional.empty();
List<OCIncludeDirective> includesInRange = new ArrayList<>();
boolean collectRange;
BuildingVisitor(OCFile file) {
this.file = file;
this.commenter = new OCCommenter();
}
IwyuPragmas build() {
return new IwyuPragmas(file, privatePragma, keeps.build(), exports.build(), associatedHeader);
}
@Override
public void visitImportDirective(OCIncludeDirective directive) {
if (collectRange) {
includesInRange.add(directive);
}
visitTrailingComments(directive);
super.visitImportDirective(directive);
}
@Override
public void visitComment(PsiComment comment) {
String text = trimCommentContent(comment.getText());
if (text.startsWith(IWYU_PREFIX)) {
String pragmaContent = StringUtil.trimStart(text, IWYU_PREFIX);
for (StandalonePragmaParser parser : STANDALONE_PARSERS) {
if (parser.tryParse(this, pragmaContent)) {
break;
}
}
}
super.visitComment(comment);
}
// In older CIDR implementations, trailing comments are not separate PsiComment nodes. They
// are simply part of the "directive content" PsiElement (which also has #include path).
// Thus, we have to handle it at the OCIncludeDirective level instead of waiting for
// visitComment() to run. In newer CIDR implementations, trailing comments are a separate
// PsiComment node, but they are still a child of the OCIncludeDirective, so pragma should be
// found in the directive's getText().
private void visitTrailingComments(OCIncludeDirective directive) {
String fullText = directive.getText();
String pathText = directive.getReferenceText();
if (pathText.isEmpty()) {
return;
}
String afterPath = fullText.substring(fullText.indexOf(pathText) + pathText.length());
OCIncludeDirective.Delimiters delimiters = directive.getDelimiters();
int delimIndex = afterPath.indexOf(delimiters.getAfterText());
if (delimIndex == -1) {
return;
}
afterPath = afterPath.substring(delimIndex + delimiters.getAfterText().length()).trim();
String trimmed = trimCommentContent(afterPath);
if (trimmed.startsWith(IWYU_PREFIX)) {
String pragmaContent = StringUtil.trimStart(trimmed, IWYU_PREFIX);
for (TrailingPragmaParser parser : TRAILING_PARSERS) {
if (parser.tryParse(this, directive, pragmaContent)) {
break;
}
}
}
}
private String trimCommentContent(String text) {
if (text.startsWith(commenter.getLineCommentPrefix())) {
return StringUtil.trimStart(text, commenter.getLineCommentPrefix()).trim();
} else if (text.startsWith(commenter.getBlockCommentPrefix())) {
return StringUtil.trimEnd(
StringUtil.trimStart(text, commenter.getBlockCommentPrefix()),
commenter.getBlockCommentSuffix())
.trim();
}
return text.trim();
}
}
}