//////////////////////////////////////////////////////////////////////////////// // checkstyle: Checks Java source code for adherence to a set of rules. // Copyright (C) 2001-2017 the original author or authors. // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //////////////////////////////////////////////////////////////////////////////// package com.puppycrawl.tools.checkstyle.checks.javadoc; import java.util.LinkedList; import java.util.List; /** * <p> * Helper class used to parse HTML tags or generic type identifiers * from a single line of text. Just the beginning of the HTML tag * is located. No attempt is made to parse out the complete tag, * particularly since some of the tag parameters could be located * on the following line of text. The {@code hasNextTag} and * {@code nextTag} methods are used to iterate through the HTML * tags or generic type identifiers that were found on the line of text. * </p> * * <p> * This class isn't really specific to HTML tags. Currently the only HTML * tag that this class looks specifically for is the HTML comment tag. * This class helps figure out if a tag exists and if it is well-formed. * It does not know whether it is valid HTML. This class is also used for * generics types which looks like opening HTML tags ex: {@code <T>, <E>, <V>, * <MY_FOO_TYPE>}, etc. According to this class they are valid tags. * </p> * * @author Chris Stillwell */ class TagParser { /** List of HtmlTags found on the input line of text. */ private final List<HtmlTag> tags = new LinkedList<>(); /** * Constructs a TagParser and finds the first tag if any. * @param text the line of text to parse. * @param lineNo the source line number. */ TagParser(String[] text, int lineNo) { parseTags(text, lineNo); } /** * Returns the next available HtmlTag. * @return a HtmlTag or {@code null} if none available. * @throws IndexOutOfBoundsException if there are no HtmlTags * left to return. */ public HtmlTag nextTag() { return tags.remove(0); } /** * Indicates if there are any more HtmlTag to retrieve. * @return {@code true} if there are more tags. */ public boolean hasNextTag() { return !tags.isEmpty(); } /** * Performs lazy initialization on the internal tags List * and adds the tag. * @param tag the HtmlTag to add. */ private void add(HtmlTag tag) { tags.add(tag); } /** * Parses the text line for any HTML tags and adds them to the internal * List of tags. * @param text the source line to parse. * @param lineNo the source line number. */ private void parseTags(String[] text, int lineNo) { final int nLines = text.length; Point position = findChar(text, '<', new Point(0, 0)); while (position.getLineNo() < nLines) { // if this is html comment then skip it if (isCommentTag(text, position)) { position = skipHtmlComment(text, position); } else if (isTag(text, position)) { position = parseTag(text, lineNo, nLines, position); } else { position = getNextCharPos(text, position); } position = findChar(text, '<', position); } } /** * Parses the tag and return position after it * @param text the source line to parse. * @param lineNo the source line number. * @param nLines line length * @param position start position for parsing * @return position after tag */ private Point parseTag(String[] text, int lineNo, final int nLines, Point position) { // find end of tag final Point endTag = findChar(text, '>', position); final boolean incompleteTag = endTag.getLineNo() >= nLines; // get tag id (one word) final String tagId; if (incompleteTag) { tagId = ""; } else { tagId = getTagId(text, position); } // is this closed tag final boolean closedTag = endTag.getLineNo() < nLines && text[endTag.getLineNo()] .charAt(endTag.getColumnNo() - 1) == '/'; // add new tag add(new HtmlTag(tagId, position.getLineNo() + lineNo, position.getColumnNo(), closedTag, incompleteTag, text[position.getLineNo()])); return endTag; } /** * Checks if the given position is start one for HTML tag. * @param javadocText text of javadoc comments. * @param pos position to check. * @return {@code true} some HTML tag starts from given position. */ private static boolean isTag(String[] javadocText, Point pos) { final int column = pos.getColumnNo() + 1; final String text = javadocText[pos.getLineNo()]; //Character.isJavaIdentifier... may not be a valid HTML //identifier but is valid for generics return column < text.length() && (Character.isJavaIdentifierStart(text.charAt(column)) || text.charAt(column) == '/') || column >= text.length(); } /** * Parse tag id. * @param javadocText text of javadoc comments. * @param tagStart start position of the tag * @return id for given tag */ private static String getTagId(String[] javadocText, Point tagStart) { String tagId = ""; int column = tagStart.getColumnNo() + 1; String text = javadocText[tagStart.getLineNo()]; if (column < text.length()) { if (text.charAt(column) == '/') { column++; } text = text.substring(column).trim(); int position = 0; //Character.isJavaIdentifier... may not be a valid HTML //identifier but is valid for generics while (position < text.length() && (Character.isJavaIdentifierStart(text.charAt(position)) || Character.isJavaIdentifierPart(text.charAt(position)))) { position++; } tagId = text.substring(0, position); } return tagId; } /** * If this is a HTML-comments. * @param text text of javadoc comments * @param pos position to check * @return {@code true} if HTML-comments * starts form given position. */ private static boolean isCommentTag(String[] text, Point pos) { return text[pos.getLineNo()].startsWith("<!--", pos.getColumnNo()); } /** * Skips HTML comments. * @param text text of javadoc comments. * @param fromPoint start position of HTML-comments * @return position after HTML-comments */ private static Point skipHtmlComment(String[] text, Point fromPoint) { Point toPoint = fromPoint; toPoint = findChar(text, '>', toPoint); while (!text[toPoint.getLineNo()] .substring(0, toPoint.getColumnNo() + 1).endsWith("-->")) { toPoint = findChar(text, '>', getNextCharPos(text, toPoint)); } return toPoint; } /** * Finds next occurrence of given character. * @param text text to search * @param character character to search * @param from position to start search * @return position of next occurrence of given character */ private static Point findChar(String[] text, char character, Point from) { Point curr = new Point(from.getLineNo(), from.getColumnNo()); while (curr.getLineNo() < text.length && text[curr.getLineNo()].charAt(curr.getColumnNo()) != character) { curr = getNextCharPos(text, curr); } return curr; } /** * Returns position of next comment character, skips * whitespaces and asterisks. * @param text to search. * @param from location to search from * @return location of the next character. */ private static Point getNextCharPos(String[] text, Point from) { int line = from.getLineNo(); int column = from.getColumnNo() + 1; while (line < text.length && column >= text[line].length()) { // go to the next line line++; column = 0; if (line < text.length) { //skip beginning spaces and stars final String currentLine = text[line]; while (column < currentLine.length() && (Character.isWhitespace(currentLine.charAt(column)) || currentLine.charAt(column) == '*')) { column++; if (column < currentLine.length() && currentLine.charAt(column - 1) == '*' && currentLine.charAt(column) == '/') { // this is end of comment column = currentLine.length(); } } } } return new Point(line, column); } /** * Represents current position in the text. * @author o_sukholsky */ private static final class Point { /** Line number. */ private final int lineNo; /** Column number.*/ private final int columnNo; /** * Creates new {@code Point} instance. * @param lineNo line number * @param columnNo column number */ Point(int lineNo, int columnNo) { this.lineNo = lineNo; this.columnNo = columnNo; } /** * Getter for line number. * @return line number of the position. */ public int getLineNo() { return lineNo; } /** * Getter for column number. * @return column number of the position. */ public int getColumnNo() { return columnNo; } } }