/* * Copyright (C) 2012 The Android Open Source Project * * 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.android.tools.lint.checks; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.tools.lint.detector.api.Category; import com.android.tools.lint.detector.api.Context; import com.android.tools.lint.detector.api.Detector; import com.android.tools.lint.detector.api.Implementation; import com.android.tools.lint.detector.api.Issue; import com.android.tools.lint.detector.api.JavaContext; import com.android.tools.lint.detector.api.Location; import com.android.tools.lint.detector.api.Scope; import com.android.tools.lint.detector.api.Severity; import com.android.tools.lint.detector.api.Speed; import java.io.File; import java.util.Collections; import java.util.List; import lombok.ast.AstVisitor; import lombok.ast.Comment; import lombok.ast.ForwardingAstVisitor; import lombok.ast.Node; /** * Looks for issues in Java comments */ public class CommentDetector extends Detector implements Detector.JavaScanner { private static final String STOPSHIP_COMMENT = "STOPSHIP"; //$NON-NLS-1$ private static final Implementation IMPLEMENTATION = new Implementation( CommentDetector.class, Scope.JAVA_FILE_SCOPE); /** Looks for hidden code */ public static final Issue EASTER_EGG = Issue.create( "EasterEgg", //$NON-NLS-1$ "Code contains easter egg", "An \"easter egg\" is code deliberately hidden in the code, both from potential " + "users and even from other developers. This lint check looks for code which " + "looks like it may be hidden from sight.", Category.SECURITY, 6, Severity.WARNING, IMPLEMENTATION) .setEnabledByDefault(false); /** Looks for special comment markers intended to stop shipping the code */ public static final Issue STOP_SHIP = Issue.create( "StopShip", //$NON-NLS-1$ "Code contains `STOPSHIP` marker", "Using the comment `// STOPSHIP` can be used to flag code that is incomplete but " + "checked in. This comment marker can be used to indicate that the code should not " + "be shipped until the issue is addressed, and lint will look for these.", Category.CORRECTNESS, 10, Severity.WARNING, IMPLEMENTATION) .setEnabledByDefault(false); private static final String ESCAPE_STRING = "\\u002a\\u002f"; //$NON-NLS-1$ /** Lombok's AST only passes comment nodes for Javadoc so I need to do manual token scanning instead */ private static final boolean USE_AST = false; /** Constructs a new {@link CommentDetector} check */ public CommentDetector() { } @Override public boolean appliesTo(@NonNull Context context, @NonNull File file) { return true; } @NonNull @Override public Speed getSpeed() { return Speed.NORMAL; } @Override public List<Class<? extends Node>> getApplicableNodeTypes() { if (USE_AST) { return Collections.<Class<? extends Node>>singletonList(Comment.class); } else { return null; } } @Override public AstVisitor createJavaVisitor(@NonNull JavaContext context) { // Lombok does not generate comment nodes for block and line comments, only for // javadoc comments! if (USE_AST) { return new CommentChecker(context); } else { String source = context.getContents(); if (source == null) { return null; } // Process the Java source such that we pass tokens to it for (int i = 0, n = source.length() - 1; i < n; i++) { char c = source.charAt(i); if (c == '\\') { i += 1; } else if (c == '/') { char next = source.charAt(i + 1); if (next == '/') { // Line comment int start = i + 2; int end = source.indexOf('\n', start); if (end == -1) { end = n; } checkComment(context, null, source, 0, start, end); } else if (next == '*') { // Block comment int start = i + 2; int end = source.indexOf("*/", start); if (end == -1) { end = n; } checkComment(context, null, source, 0, start, end); } } } return null; } } private static class CommentChecker extends ForwardingAstVisitor { private final JavaContext mContext; public CommentChecker(JavaContext context) { mContext = context; } @Override public boolean visitComment(Comment node) { String contents = node.astContent(); checkComment(mContext, node, contents, node.getPosition().getStart(), 0, contents.length()); return super.visitComment(node); } } private static void checkComment( @NonNull JavaContext context, @Nullable Comment node, @NonNull String source, int offset, int start, int end) { char prev = 0; char c; for (int i = start; i < end - 2; i++, prev = c) { c = source.charAt(i); if (prev == '\\') { if (c == 'u' || c == 'U') { if (source.regionMatches(true, i - 1, ESCAPE_STRING, 0, ESCAPE_STRING.length())) { Location location = Location.create(context.file, source, offset + i - 1, offset + i - 1 + ESCAPE_STRING.length()); context.report(EASTER_EGG, node, location, "Code might be hidden here; found unicode escape sequence " + "which is interpreted as comment end, compiled code follows"); } } else { i++; } } else if (prev == 'S' && c == 'T' && source.regionMatches(i - 1, STOPSHIP_COMMENT, 0, STOPSHIP_COMMENT.length())) { // TODO: Only flag this issue in release mode?? Location location = Location.create(context.file, source, offset + i - 1, offset + i - 1 + STOPSHIP_COMMENT.length()); context.report(STOP_SHIP, node, location, "`STOPSHIP` comment found; points to code which must be fixed prior " + "to release"); } } } }