1027 lines
39 KiB
Java
1027 lines
39 KiB
Java
|
|
/*
|
|
* Janino - An embedded Java[TM] compiler
|
|
*
|
|
* Copyright (c) 2001-2010, Arno Unkrig
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
|
|
* following conditions are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
|
|
* following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
|
|
* following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
* 3. The name of the author may not be used to endorse or promote products derived from this software without
|
|
* specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
|
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
|
* THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
* POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
package org.codehaus.janino;
|
|
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileWriter;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.io.Reader;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
|
|
import org.codehaus.commons.compiler.CompileException;
|
|
import org.codehaus.commons.compiler.ICookable;
|
|
import org.codehaus.commons.compiler.Location;
|
|
import org.codehaus.commons.compiler.WarningHandler;
|
|
import org.codehaus.janino.util.TeeReader;
|
|
|
|
/**
|
|
* Splits up a character stream into tokens and returns them as
|
|
* {@link java.lang.String String} objects.
|
|
* <p>
|
|
* The <code>optionalFileName</code> parameter passed to many
|
|
* constructors should point
|
|
*/
|
|
@SuppressWarnings({ "rawtypes", "unchecked" }) public
|
|
class Scanner {
|
|
|
|
// Public Scanners that read from a file.
|
|
|
|
/**
|
|
* Set up a scanner that reads tokens from the given file in the default charset.
|
|
* <p>
|
|
* <b>This method is deprecated because it leaves the input file open.</b>
|
|
*
|
|
* @deprecated // SUPPRESS CHECKSTYLE MissingDeprecated
|
|
*/
|
|
@Deprecated public
|
|
Scanner(String fileName) throws CompileException, IOException {
|
|
this(
|
|
fileName, // optionalFileName
|
|
new FileInputStream(fileName) // is
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Set up a scanner that reads tokens from the given file in the given encoding.
|
|
* <p>
|
|
* <b>This method is deprecated because it leaves the input file open.</b>
|
|
*
|
|
* @deprecated // SUPPRESS CHECKSTYLE MissingDeprecated
|
|
*/
|
|
@Deprecated public
|
|
Scanner(String fileName, String encoding) throws CompileException, IOException {
|
|
this(
|
|
fileName, // optionalFileName
|
|
new FileInputStream(fileName), // is
|
|
encoding // optionalEncoding
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Set up a scanner that reads tokens from the given file in the platform
|
|
* default encoding.
|
|
* <p>
|
|
* <b>This method is deprecated because it leaves the input file open.</b>
|
|
*
|
|
* @deprecated // SUPPRESS CHECKSTYLE MissingDeprecated
|
|
*/
|
|
@Deprecated public
|
|
Scanner(File file) throws CompileException, IOException {
|
|
this(
|
|
file.getAbsolutePath(), // optionalFileName
|
|
new FileInputStream(file), // is
|
|
null // optionalEncoding
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Set up a scanner that reads tokens from the given file in the given encoding.
|
|
* <p>
|
|
* <b>This method is deprecated because it leaves the input file open.</b>
|
|
*
|
|
* @deprecated // SUPPRESS CHECKSTYLE MissingDeprecated
|
|
*/
|
|
@Deprecated public
|
|
Scanner(File file, String optionalEncoding) throws CompileException, IOException {
|
|
this(
|
|
file.getAbsolutePath(), // optionalFileName
|
|
new FileInputStream(file), // is
|
|
optionalEncoding // optionalEncoding
|
|
);
|
|
}
|
|
|
|
// Public Scanners that read from an InputStream
|
|
|
|
/**
|
|
* Set up a scanner that reads tokens from the given
|
|
* {@link InputStream} in the platform default encoding.
|
|
* <p>
|
|
* The <code>fileName</code> is solely used for reporting in thrown
|
|
* exceptions.
|
|
*/
|
|
public
|
|
Scanner(String optionalFileName, InputStream is) throws CompileException, IOException {
|
|
this(
|
|
optionalFileName,
|
|
new InputStreamReader(is), // in
|
|
(short) 1, // initialLineNumber
|
|
(short) 0 // initialColumnNumber
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Set up a scanner that reads tokens from the given
|
|
* {@link InputStream} with the given <code>optionalEncoding</code>
|
|
* (<code>null</code> means platform default encoding).
|
|
* <p>
|
|
* The <code>optionalFileName</code> is used for reporting errors during
|
|
* compilation and for source level debugging, and should name an existing
|
|
* file. If <code>null</code> is passed, and the system property
|
|
* <code>org.codehaus.janino.source_debugging.enable</code> is set to "true", then
|
|
* a temporary file in <code>org.codehaus.janino.source_debugging.dir</code> or the
|
|
* system's default temp dir is created in order to make the source code
|
|
* available to a debugger.
|
|
*/
|
|
public
|
|
Scanner(String optionalFileName, InputStream is, String optionalEncoding) throws CompileException, IOException {
|
|
this(
|
|
optionalFileName, // optionalFileName
|
|
( // in
|
|
optionalEncoding == null
|
|
? new InputStreamReader(is)
|
|
: new InputStreamReader(is, optionalEncoding)
|
|
),
|
|
(short) 1, // initialLineNumber
|
|
(short) 0 // initialColumnNumber
|
|
);
|
|
}
|
|
|
|
// Public Scanners that read from a Reader.
|
|
|
|
/**
|
|
* Set up a scanner that reads tokens from the given
|
|
* {@link Reader}.
|
|
* <p>
|
|
* The <code>optionalFileName</code> is used for reporting errors during
|
|
* compilation and for source level debugging, and should name an existing
|
|
* file. If <code>null</code> is passed, and the system property
|
|
* <code>org.codehaus.janino.source_debugging.enable</code> is set to "true", then
|
|
* a temporary file in <code>org.codehaus.janino.source_debugging.dir</code> or the
|
|
* system's default temp dir is created in order to make the source code
|
|
* available to a debugger.
|
|
*/
|
|
public
|
|
Scanner(String optionalFileName, Reader in) throws CompileException, IOException {
|
|
this(
|
|
optionalFileName, // optionalFileName
|
|
in, // in
|
|
(short) 1, // initialLineNumber
|
|
(short) 0 // initialColumnNumber
|
|
);
|
|
}
|
|
|
|
/** Creates a {@link Scanner} that counts lines and columns from non-default initial values. */
|
|
public
|
|
Scanner(
|
|
String optionalFileName,
|
|
Reader in,
|
|
short initialLineNumber, // "1" is a good idea
|
|
short initialColumnNumber // "0" is a good idea
|
|
) throws CompileException, IOException {
|
|
|
|
// Debugging on source code level is only possible if the code comes from
|
|
// a "real" Java source file which the debugger can read. If this is not the
|
|
// case, and we absolutely want source code level debugging, then we write
|
|
// a verbatim copy of the source code into a temporary file in the system
|
|
// temp directory.
|
|
// This behavior is controlled by the two system properties
|
|
// org.codehaus.janino.source_debugging.enable
|
|
// org.codehaus.janino.source_debugging.dir
|
|
// JANINO is designed to compile in memory to save the overhead of disk
|
|
// I/O, so writing this file is only recommended for source code level
|
|
// debugging purposes.
|
|
if (optionalFileName == null && Boolean.getBoolean(ICookable.SYSTEM_PROPERTY_SOURCE_DEBUGGING_ENABLE)) {
|
|
String dirName = System.getProperty(ICookable.SYSTEM_PROPERTY_SOURCE_DEBUGGING_DIR);
|
|
File dir = dirName == null ? null : new File(dirName);
|
|
File temporaryFile = File.createTempFile("janino", ".java", dir);
|
|
temporaryFile.deleteOnExit();
|
|
in = new TeeReader(
|
|
in, // in
|
|
new FileWriter(temporaryFile), // out
|
|
true // closeWriterOnEOF
|
|
);
|
|
optionalFileName = temporaryFile.getAbsolutePath();
|
|
}
|
|
|
|
this.optionalFileName = optionalFileName;
|
|
this.in = new UnicodeUnescapeReader(in);
|
|
this.nextCharLineNumber = initialLineNumber;
|
|
this.nextCharColumnNumber = initialColumnNumber;
|
|
|
|
this.readNextChar();
|
|
}
|
|
|
|
/** @return The file name optionally passed to the constructor */
|
|
public String
|
|
getFileName() { return this.optionalFileName; }
|
|
|
|
/**
|
|
* Closes the character source (file, {@link InputStream}, {@link Reader}) associated with this object. The results
|
|
* of future calls to {@link #produce()} are undefined.
|
|
*
|
|
* @deprecated This method is deprecated, because the concept described above is confusing. An application should
|
|
* close the underlying {@link InputStream} or {@link Reader} itself.</b>
|
|
*/
|
|
@Deprecated public void
|
|
close() throws IOException { this.in.close(); }
|
|
|
|
/**
|
|
* Get the text of the doc comment (a.k.a. "JAVADOC comment") preceeding
|
|
* the next token.
|
|
* @return <code>null</code> if the next token is not preceeded by a doc comment
|
|
*/
|
|
public String
|
|
doc() {
|
|
String s = this.docComment;
|
|
this.docComment = null;
|
|
return s;
|
|
}
|
|
|
|
/** @return The {@link Location} of the next character */
|
|
public Location
|
|
location() {
|
|
return new Location(this.optionalFileName, this.nextCharLineNumber, this.nextCharColumnNumber);
|
|
}
|
|
|
|
/** Representation of a Java™ token. */
|
|
public final
|
|
class Token {
|
|
private final String optionalFileName;
|
|
private final short lineNumber;
|
|
private final short columnNumber;
|
|
private Location location;
|
|
|
|
/** The type of this token; legal values are the various public constant declared in this class. */
|
|
public final int type;
|
|
|
|
/** Indication of the 'end-of-input' condition. */
|
|
public static final int EOF = 0;
|
|
|
|
/** The token represents an identifier. */
|
|
public static final int IDENTIFIER = 1;
|
|
|
|
/** The token represents a keyword. */
|
|
public static final int KEYWORD = 2;
|
|
|
|
/**
|
|
* The token represents an integer literal; its {@link #value} is the text of the integer literal exactly as it
|
|
* appears in the source code.
|
|
*/
|
|
public static final int INTEGER_LITERAL = 3;
|
|
|
|
/**
|
|
* The token represents a floating-point literal; its {@link #value} is the text of the floating-point literal
|
|
* exactly as it appears in the source code.
|
|
*/
|
|
public static final int FLOATING_POINT_LITERAL = 4;
|
|
|
|
/** The token represents a boolean literal; its {@link #value} is either 'true' or 'false'. */
|
|
public static final int BOOLEAN_LITERAL = 5;
|
|
|
|
/**
|
|
* The token represents a character literal; its {@link #value} is the text of the character literal exactly as
|
|
* it appears in the source code (including the single quotes around it).
|
|
*/
|
|
public static final int CHARACTER_LITERAL = 6;
|
|
|
|
/**
|
|
* The token represents a string literal; its {@link #value} is the text of the string literal exactly as it
|
|
* appears in the source code (including the double quotes around it).
|
|
*/
|
|
public static final int STRING_LITERAL = 7;
|
|
|
|
/** The token represents the {@code null} literal; its {@link #value} is 'null'. */
|
|
public static final int NULL_LITERAL = 8;
|
|
|
|
/** The token represents an operator; its {@link #value} is exactly the particular operator (e.g. "<<<="). */
|
|
public static final int OPERATOR = 9;
|
|
|
|
/** The text of the token exactly as it appears in the source code. */
|
|
public final String value;
|
|
|
|
private
|
|
Token(int type, String value) {
|
|
this.optionalFileName = Scanner.this.optionalFileName;
|
|
this.lineNumber = Scanner.this.tokenLineNumber;
|
|
this.columnNumber = Scanner.this.tokenColumnNumber;
|
|
this.type = type;
|
|
this.value = value;
|
|
}
|
|
|
|
/** @return The location of the first character of this token */
|
|
public Location
|
|
getLocation() {
|
|
if (this.location == null) {
|
|
this.location = new Location(this.optionalFileName, this.lineNumber, this.columnNumber);
|
|
}
|
|
return this.location;
|
|
}
|
|
|
|
@Override public String
|
|
toString() { return this.value; }
|
|
}
|
|
|
|
/**
|
|
* Preduces and returns the next token. Notice that end-of-input is <i>not</i> signalized with a {@code null}
|
|
* product, but by the special {@link Token#EOF} token.
|
|
*/
|
|
public Token
|
|
produce() throws CompileException, IOException {
|
|
if (this.docComment != null) {
|
|
this.warning("MDC", "Misplaced doc comment", this.location());
|
|
this.docComment = null;
|
|
}
|
|
|
|
// Skip whitespace and process comments.
|
|
int state = 0;
|
|
StringBuilder dcsb = null; // For doc comment
|
|
|
|
PROCESS_COMMENTS:
|
|
for (;;) {
|
|
switch (state) {
|
|
|
|
case 0: // Outside any comment
|
|
if (this.nextChar == -1) {
|
|
return new Token(Token.EOF, "EOF");
|
|
} else
|
|
if (Character.isWhitespace((char) this.nextChar)) {
|
|
;
|
|
} else
|
|
if (this.nextChar == '/') {
|
|
state = 1;
|
|
} else
|
|
{
|
|
break PROCESS_COMMENTS;
|
|
}
|
|
break;
|
|
|
|
case 1: // After "/"
|
|
if (this.nextChar == -1) {
|
|
return new Token(Token.OPERATOR, "/");
|
|
} else
|
|
if (this.nextChar == '=') {
|
|
this.readNextChar();
|
|
return new Token(Token.OPERATOR, "/=");
|
|
} else
|
|
if (this.nextChar == '/') {
|
|
state = 2;
|
|
} else
|
|
if (this.nextChar == '*') {
|
|
state = 3;
|
|
} else
|
|
{
|
|
return new Token(Token.OPERATOR, "/");
|
|
}
|
|
break;
|
|
|
|
case 2: // After "//..."
|
|
if (this.nextChar == -1) {
|
|
return new Token(Token.EOF, "EOF");
|
|
} else
|
|
if (this.nextChar == '\r' || this.nextChar == '\n') {
|
|
state = 0;
|
|
} else
|
|
{
|
|
;
|
|
}
|
|
break;
|
|
|
|
case 3: // After "/*"
|
|
if (this.nextChar == -1) {
|
|
throw new CompileException("EOF in traditional comment", this.location());
|
|
} else
|
|
if (this.nextChar == '*') {
|
|
state = 4;
|
|
} else
|
|
{
|
|
state = 9;
|
|
}
|
|
break;
|
|
|
|
case 4: // After "/**"
|
|
if (this.nextChar == -1) {
|
|
throw new CompileException("EOF in doc comment", this.location());
|
|
} else
|
|
if (this.nextChar == '/') {
|
|
state = 0;
|
|
} else
|
|
{
|
|
if (this.docComment != null) {
|
|
this.warning(
|
|
"MDC",
|
|
"Multiple doc comments",
|
|
new Location(this.optionalFileName, this.nextCharLineNumber, this.nextCharColumnNumber)
|
|
);
|
|
}
|
|
dcsb = new StringBuilder().append((char) this.nextChar);
|
|
state = (
|
|
(this.nextChar == '\r' || this.nextChar == '\n') ? 6
|
|
: this.nextChar == '*' ? 8
|
|
: 5
|
|
);
|
|
}
|
|
break;
|
|
|
|
case 5: // After "/**..."
|
|
if (this.nextChar == -1) {
|
|
throw new CompileException("EOF in doc comment", this.location());
|
|
} else
|
|
if (this.nextChar == '*') {
|
|
state = 8;
|
|
} else
|
|
if (this.nextChar == '\r' || this.nextChar == '\n') {
|
|
dcsb.append((char) this.nextChar);
|
|
state = 6;
|
|
} else
|
|
{
|
|
dcsb.append((char) this.nextChar);
|
|
}
|
|
break;
|
|
|
|
case 6: // After "/**...\n"
|
|
if (this.nextChar == -1) {
|
|
throw new CompileException("EOF in doc comment", this.location());
|
|
} else
|
|
if (this.nextChar == '*') {
|
|
state = 7;
|
|
} else
|
|
if (this.nextChar == '\r' || this.nextChar == '\n') {
|
|
dcsb.append((char) this.nextChar);
|
|
} else
|
|
if (this.nextChar == ' ' || this.nextChar == '\t') {
|
|
;
|
|
} else
|
|
{
|
|
dcsb.append((char) this.nextChar);
|
|
state = 5;
|
|
}
|
|
break;
|
|
|
|
case 7: // After "/**...\n *"
|
|
if (this.nextChar == -1) {
|
|
throw new CompileException("EOF in doc comment", this.location());
|
|
} else
|
|
if (this.nextChar == '*') {
|
|
;
|
|
} else
|
|
if (this.nextChar == '/') {
|
|
this.docComment = dcsb.toString();
|
|
state = 0;
|
|
} else
|
|
{
|
|
dcsb.append((char) this.nextChar);
|
|
state = 5;
|
|
}
|
|
break;
|
|
|
|
case 8: // After "/**...*"
|
|
if (this.nextChar == -1) {
|
|
throw new CompileException("EOF in doc comment", this.location());
|
|
} else
|
|
if (this.nextChar == '/') {
|
|
this.docComment = dcsb.toString();
|
|
state = 0;
|
|
} else
|
|
if (this.nextChar == '*') {
|
|
dcsb.append('*');
|
|
} else
|
|
{
|
|
dcsb.append('*');
|
|
dcsb.append((char) this.nextChar);
|
|
state = 5;
|
|
}
|
|
break;
|
|
|
|
case 9: // After "/*..."
|
|
if (this.nextChar == -1) {
|
|
throw new CompileException("EOF in traditional comment", this.location());
|
|
} else
|
|
if (this.nextChar == '*') {
|
|
state = 10;
|
|
} else
|
|
{
|
|
;
|
|
}
|
|
break;
|
|
|
|
case 10: // After "/*...*"
|
|
if (this.nextChar == -1) {
|
|
throw new CompileException("EOF in traditional comment", this.location());
|
|
} else
|
|
if (this.nextChar == '/') {
|
|
state = 0;
|
|
} else
|
|
if (this.nextChar == '*') {
|
|
;
|
|
} else
|
|
{
|
|
state = 9;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
throw new JaninoRuntimeException(Integer.toString(state));
|
|
}
|
|
this.readNextChar();
|
|
}
|
|
|
|
/*
|
|
* Whitespace and comments are now skipped; "nextChar" is definitely
|
|
* the first character of the token.
|
|
*/
|
|
this.tokenLineNumber = this.nextCharLineNumber;
|
|
this.tokenColumnNumber = this.nextCharColumnNumber;
|
|
|
|
// Scan identifier.
|
|
if (Character.isJavaIdentifierStart((char) this.nextChar)) {
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.append((char) this.nextChar);
|
|
for (;;) {
|
|
this.readNextChar();
|
|
if (this.nextChar == -1 || !Character.isJavaIdentifierPart((char) this.nextChar)) break;
|
|
sb.append((char) this.nextChar);
|
|
}
|
|
String s = sb.toString();
|
|
if ("true".equals(s)) return new Token(Token.BOOLEAN_LITERAL, "true");
|
|
if ("false".equals(s)) return new Token(Token.BOOLEAN_LITERAL, "false");
|
|
if ("null".equals(s)) return new Token(Token.NULL_LITERAL, "null");
|
|
{
|
|
String v = (String) Scanner.JAVA_KEYWORDS.get(s);
|
|
if (v != null) return new Token(Token.KEYWORD, v);
|
|
}
|
|
return new Token(Token.IDENTIFIER, s);
|
|
}
|
|
|
|
// Scan numeric literal.
|
|
if (Character.isDigit((char) this.nextChar)) {
|
|
return this.scanNumericLiteral(false);
|
|
}
|
|
|
|
// A "." is special: Could either be a floating-point constant like ".001", or the "."
|
|
// operator.
|
|
if (this.nextChar == '.') {
|
|
this.readNextChar();
|
|
if (Character.isDigit((char) this.nextChar)) {
|
|
return this.scanNumericLiteral(true);
|
|
} else {
|
|
return new Token(Token.OPERATOR, ".");
|
|
}
|
|
}
|
|
|
|
// Scan string literal.
|
|
if (this.nextChar == '"') {
|
|
StringBuilder sb = new StringBuilder("\"");
|
|
this.readNextChar();
|
|
while (this.nextChar != '"') {
|
|
this.scanLiteralCharacter(sb);
|
|
}
|
|
this.readNextChar();
|
|
return new Token(Token.STRING_LITERAL, sb.append('"').toString());
|
|
}
|
|
|
|
// Scan character literal.
|
|
if (this.nextChar == '\'') {
|
|
this.readNextChar();
|
|
if (this.nextChar == '\'') {
|
|
throw new CompileException(
|
|
"Single quote must be backslash-escaped in character literal",
|
|
this.location()
|
|
);
|
|
}
|
|
|
|
StringBuilder sb = new StringBuilder("'");
|
|
this.scanLiteralCharacter(sb);
|
|
if (this.nextChar != '\'') throw new CompileException("Closing single quote missing", this.location());
|
|
this.readNextChar();
|
|
|
|
return new Token(Token.CHARACTER_LITERAL, sb.append('\'').toString());
|
|
}
|
|
|
|
// Scan separator / operator.
|
|
{
|
|
String v = (String) Scanner.JAVA_OPERATORS.get(new String(new char[] { (char) this.nextChar }));
|
|
if (v != null) {
|
|
for (;;) {
|
|
this.readNextChar();
|
|
String v2 = (String) (
|
|
this.expectGreater
|
|
? Scanner.JAVA_OPERATORS_EXPECT_GREATER
|
|
: Scanner.JAVA_OPERATORS
|
|
).get(v + (char) this.nextChar);
|
|
if (v2 == null) return new Token(Token.OPERATOR, v);
|
|
v = v2;
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new CompileException(
|
|
"Invalid character input \"" + (char) this.nextChar + "\" (character code " + this.nextChar + ")",
|
|
this.location()
|
|
);
|
|
}
|
|
|
|
/** @return Whether the scanner is currently in 'expect greater' mode */
|
|
public boolean
|
|
getExpectGreater() { return this.expectGreater; }
|
|
|
|
/**
|
|
* Sets or resets the 'expect greater' mode.
|
|
*
|
|
* @return Whether the 'expect greater' mode was previously active
|
|
*/
|
|
public boolean
|
|
setExpectGreater(boolean value) {
|
|
boolean tmp = this.expectGreater;
|
|
this.expectGreater = value;
|
|
return tmp;
|
|
}
|
|
|
|
private Token
|
|
scanNumericLiteral(boolean hadDecimalPoint) throws CompileException, IOException {
|
|
StringBuilder sb = hadDecimalPoint ? new StringBuilder(".") : new StringBuilder();
|
|
int state = hadDecimalPoint ? 2 : 0;
|
|
for (;;) {
|
|
switch (state) {
|
|
|
|
case 0: // First character.
|
|
if (this.nextChar == '0') {
|
|
sb.append('0');
|
|
state = 6;
|
|
} else
|
|
if (Character.isDigit((char) this.nextChar)) {
|
|
sb.append((char) this.nextChar);
|
|
state = 1;
|
|
} else
|
|
{
|
|
throw new CompileException(
|
|
"Numeric literal begins with invalid character '" + (char) this.nextChar + "'",
|
|
this.location()
|
|
);
|
|
}
|
|
break;
|
|
|
|
case 1: // Decimal digits.
|
|
if (Character.isDigit((char) this.nextChar)) {
|
|
sb.append((char) this.nextChar);
|
|
} else
|
|
if (this.nextChar == 'l' || this.nextChar == 'L') {
|
|
sb.append((char) this.nextChar);
|
|
this.readNextChar();
|
|
return new Token(Token.INTEGER_LITERAL, sb.toString());
|
|
} else
|
|
if (this.nextChar == 'f' || this.nextChar == 'F' || this.nextChar == 'd' || this.nextChar == 'D') {
|
|
sb.append((char) this.nextChar);
|
|
this.readNextChar();
|
|
return new Token(Token.FLOATING_POINT_LITERAL, sb.toString());
|
|
} else
|
|
if (this.nextChar == '.') {
|
|
sb.append('.');
|
|
state = 2;
|
|
} else
|
|
if (this.nextChar == 'E' || this.nextChar == 'e') {
|
|
sb.append('E');
|
|
state = 3;
|
|
} else
|
|
{
|
|
return new Token(Token.INTEGER_LITERAL, sb.toString());
|
|
}
|
|
break;
|
|
|
|
case 2: // After decimal point.
|
|
if (Character.isDigit((char) this.nextChar)) {
|
|
sb.append((char) this.nextChar);
|
|
} else
|
|
if (this.nextChar == 'e' || this.nextChar == 'E') {
|
|
sb.append('E');
|
|
state = 3;
|
|
} else
|
|
if (this.nextChar == 'f' || this.nextChar == 'F' || this.nextChar == 'd' || this.nextChar == 'D') {
|
|
sb.append((char) this.nextChar);
|
|
this.readNextChar();
|
|
return new Token(Token.FLOATING_POINT_LITERAL, sb.toString());
|
|
} else
|
|
{
|
|
return new Token(Token.FLOATING_POINT_LITERAL, sb.toString());
|
|
}
|
|
break;
|
|
|
|
case 3: // Read exponent.
|
|
if (Character.isDigit((char) this.nextChar)) {
|
|
sb.append((char) this.nextChar);
|
|
state = 5;
|
|
} else
|
|
if (this.nextChar == '-' || this.nextChar == '+') {
|
|
sb.append((char) this.nextChar);
|
|
state = 4;
|
|
} else
|
|
{
|
|
throw new CompileException("Exponent missing after \"E\"", this.location());
|
|
}
|
|
break;
|
|
|
|
case 4: // After exponent sign.
|
|
if (Character.isDigit((char) this.nextChar)) {
|
|
sb.append((char) this.nextChar);
|
|
state = 5;
|
|
} else
|
|
{
|
|
throw new CompileException("Exponent missing after 'E' and sign", this.location());
|
|
}
|
|
break;
|
|
|
|
case 5: // After first exponent digit.
|
|
if (Character.isDigit((char) this.nextChar)) {
|
|
sb.append((char) this.nextChar);
|
|
} else
|
|
if (this.nextChar == 'f' || this.nextChar == 'F' || this.nextChar == 'd' || this.nextChar == 'D') {
|
|
sb.append((char) this.nextChar);
|
|
this.readNextChar();
|
|
return new Token(Token.FLOATING_POINT_LITERAL, sb.toString());
|
|
} else
|
|
{
|
|
return new Token(Token.FLOATING_POINT_LITERAL, sb.toString());
|
|
}
|
|
break;
|
|
|
|
case 6: // After leading zero
|
|
if ("01234567".indexOf(this.nextChar) != -1) {
|
|
sb.append((char) this.nextChar);
|
|
state = 7;
|
|
} else
|
|
if (this.nextChar == 'l' || this.nextChar == 'L') {
|
|
sb.append((char) this.nextChar);
|
|
this.readNextChar();
|
|
return new Token(Token.INTEGER_LITERAL, sb.toString());
|
|
} else
|
|
if (this.nextChar == 'f' || this.nextChar == 'F' || this.nextChar == 'd' || this.nextChar == 'D') {
|
|
sb.append((char) this.nextChar);
|
|
this.readNextChar();
|
|
return new Token(Token.FLOATING_POINT_LITERAL, sb.toString());
|
|
} else
|
|
if (this.nextChar == '.') {
|
|
sb.append('.');
|
|
state = 2;
|
|
} else
|
|
if (this.nextChar == 'E' || this.nextChar == 'e') {
|
|
sb.append((char) this.nextChar);
|
|
state = 3;
|
|
} else
|
|
if (this.nextChar == 'x' || this.nextChar == 'X') {
|
|
sb.append((char) this.nextChar);
|
|
state = 8;
|
|
} else
|
|
{
|
|
return new Token(Token.INTEGER_LITERAL, "0");
|
|
}
|
|
break;
|
|
|
|
case 7: // In octal literal.
|
|
if ("01234567".indexOf(this.nextChar) != -1) {
|
|
sb.append((char) this.nextChar);
|
|
} else
|
|
if (this.nextChar == '8' || this.nextChar == '9') {
|
|
throw new CompileException(
|
|
"Digit '" + (char) this.nextChar + "' not allowed in octal literal",
|
|
this.location()
|
|
);
|
|
} else
|
|
if (this.nextChar == 'l' || this.nextChar == 'L') {
|
|
// Octal long literal.
|
|
sb.append((char) this.nextChar);
|
|
this.readNextChar();
|
|
return new Token(Token.INTEGER_LITERAL, sb.toString());
|
|
} else
|
|
{
|
|
// Octal int literal
|
|
return new Token(Token.INTEGER_LITERAL, sb.toString());
|
|
}
|
|
break;
|
|
|
|
case 8: // After '0x'.
|
|
if (Character.digit((char) this.nextChar, 16) != -1) {
|
|
sb.append((char) this.nextChar);
|
|
state = 9;
|
|
} else
|
|
{
|
|
throw new CompileException("Hex digit expected after \"0x\"", this.location());
|
|
}
|
|
break;
|
|
|
|
case 9: // After first hex digit.
|
|
if (Character.digit((char) this.nextChar, 16) != -1) {
|
|
sb.append((char) this.nextChar);
|
|
} else
|
|
if (this.nextChar == 'l' || this.nextChar == 'L') {
|
|
// Hex long literal
|
|
sb.append((char) this.nextChar);
|
|
this.readNextChar();
|
|
return new Token(Token.INTEGER_LITERAL, sb.toString());
|
|
} else
|
|
{
|
|
// Hex int literal
|
|
return new Token(Token.INTEGER_LITERAL, sb.toString());
|
|
}
|
|
break;
|
|
|
|
default:
|
|
throw new JaninoRuntimeException(Integer.toString(state));
|
|
}
|
|
this.readNextChar();
|
|
}
|
|
}
|
|
|
|
/** Scans the next literal character into a {@link StringBuilder}. */
|
|
private void
|
|
scanLiteralCharacter(StringBuilder sb) throws CompileException, IOException {
|
|
if (this.nextChar == -1) throw new CompileException("EOF in literal", this.location());
|
|
|
|
if (this.nextChar == '\r' || this.nextChar == '\n') {
|
|
throw new CompileException("Line break in literal not allowed", this.location());
|
|
}
|
|
|
|
if (this.nextChar != '\\') {
|
|
|
|
// Not an escape sequence.
|
|
sb.append((char) this.nextChar);
|
|
this.readNextChar();
|
|
return;
|
|
}
|
|
|
|
// JLS7 3.10.6: Escape sequences for character and string literals.
|
|
sb.append('\\');
|
|
this.readNextChar();
|
|
|
|
{
|
|
int idx = "btnfr\"'\\".indexOf(this.nextChar);
|
|
if (idx != -1) {
|
|
|
|
// "\t" and friends.
|
|
sb.append((char) this.nextChar);
|
|
this.readNextChar();
|
|
return;
|
|
}
|
|
}
|
|
|
|
{
|
|
int idx = "01234567".indexOf(this.nextChar);
|
|
if (idx != -1) {
|
|
|
|
// Octal escapes: "\0" through "\3ff".
|
|
char firstChar = (char) this.nextChar;
|
|
sb.append(firstChar);
|
|
this.readNextChar();
|
|
|
|
idx = "01234567".indexOf(this.nextChar);
|
|
if (idx == -1) return;
|
|
sb.append((char) this.nextChar);
|
|
this.readNextChar();
|
|
|
|
idx = "01234567".indexOf(this.nextChar);
|
|
if (idx == -1) return;
|
|
if ("0123".indexOf(firstChar) == -1) {
|
|
throw new CompileException("Invalid octal escape", this.location());
|
|
}
|
|
sb.append((char) this.nextChar);
|
|
this.readNextChar();
|
|
return;
|
|
}
|
|
}
|
|
|
|
throw new CompileException("Invalid escape sequence", this.location());
|
|
}
|
|
|
|
// Read one character and store in "nextChar".
|
|
private void
|
|
readNextChar() throws IOException, CompileException {
|
|
try {
|
|
this.nextChar = this.in.read();
|
|
} catch (UnicodeUnescapeException ex) {
|
|
throw new CompileException(ex.getMessage(), this.location(), ex);
|
|
}
|
|
if (this.nextChar == '\r') {
|
|
++this.nextCharLineNumber;
|
|
this.nextCharColumnNumber = 0;
|
|
this.crLfPending = true;
|
|
} else
|
|
if (this.nextChar == '\n') {
|
|
if (this.crLfPending) {
|
|
this.crLfPending = false;
|
|
} else {
|
|
++this.nextCharLineNumber;
|
|
this.nextCharColumnNumber = 0;
|
|
}
|
|
} else
|
|
{
|
|
++this.nextCharColumnNumber;
|
|
}
|
|
//System.out.println("'" + (char) nextChar + "' = " + (int) nextChar);
|
|
}
|
|
|
|
private final String optionalFileName;
|
|
private final Reader in;
|
|
private int nextChar = -1; // Always valid (one character read-ahead).
|
|
private boolean crLfPending;
|
|
private short nextCharLineNumber;
|
|
private short nextCharColumnNumber;
|
|
|
|
/** Line number of the previously produced token (typically starting at one). */
|
|
private short tokenLineNumber;
|
|
|
|
/**
|
|
* Column number of the first character of the previously produced token (1 if token is immediately preceded by a
|
|
* line break).
|
|
*/
|
|
private short tokenColumnNumber;
|
|
|
|
/** The optional JAVADOC comment preceding the {@link #nextToken}. */
|
|
private String docComment;
|
|
|
|
/**
|
|
* Whether the scanner is in 'expect greater' mode: If so, it parses character sequences like ">>>=" as
|
|
* ">", ">", ">", "=".
|
|
*/
|
|
private boolean expectGreater;
|
|
|
|
private static final Map<String, String> JAVA_KEYWORDS = new HashMap();
|
|
static {
|
|
String[] ks = {
|
|
"abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue",
|
|
"default", "do", "double", "else", "extends", "final", "finally", "float", "for", "goto", "if",
|
|
"implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private",
|
|
"protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this",
|
|
"throw", "throws", "transient", "try", "void", "volatile", "while",
|
|
};
|
|
for (int i = 0; i < ks.length; ++i) Scanner.JAVA_KEYWORDS.put(ks[i], ks[i]);
|
|
}
|
|
private static final Map<String, String> JAVA_OPERATORS = new HashMap();
|
|
private static final Map<String, String> JAVA_OPERATORS_EXPECT_GREATER = new HashMap();
|
|
static {
|
|
String[] ops = {
|
|
// Separators:
|
|
"(", ")", "{", "}", "[", "]", ";", ",", ".", "@",
|
|
// Operators:
|
|
"=", ">", "<", "!", "~", "?", ":",
|
|
"==", "<=", ">=", "!=", "&&", "||", "++", "--",
|
|
"+", "-", "*", "/", "&", "|", "^", "%", "<<", ">>", ">>>",
|
|
"+=", "-=", "*=", "/=", "&=", "|=", "^=", "%=", "<<=", ">>=", ">>>=",
|
|
};
|
|
for (String op : ops) {
|
|
Scanner.JAVA_OPERATORS.put(op, op);
|
|
if (!op.startsWith(">>")) Scanner.JAVA_OPERATORS_EXPECT_GREATER.put(op, op);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* By default, warnings are discarded, but an application my install a
|
|
* {@link WarningHandler}.
|
|
* <p>
|
|
* Notice that there is no <code>Scanner.setErrorHandler()</code> method, but scan errors
|
|
* always throw a {@link CompileException}. The reason being is that there is no reasonable
|
|
* way to recover from scan errors and continue scanning, so there is no need to install
|
|
* a custom scan error handler.
|
|
*
|
|
* @param optionalWarningHandler <code>null</code> to indicate that no warnings be issued
|
|
*/
|
|
public void
|
|
setWarningHandler(WarningHandler optionalWarningHandler) {
|
|
this.optionalWarningHandler = optionalWarningHandler;
|
|
}
|
|
|
|
// Used for elaborate warning handling.
|
|
private WarningHandler optionalWarningHandler;
|
|
|
|
/**
|
|
* Issues a warning with the given message and location and returns. This is done through
|
|
* a {@link WarningHandler} that was installed through
|
|
* {@link #setWarningHandler(WarningHandler)}.
|
|
* <p>
|
|
* The <code>handle</code> argument qulifies the warning and is typically used by
|
|
* the {@link WarningHandler} to suppress individual warnings.
|
|
* @throws CompileException
|
|
*/
|
|
private void
|
|
warning(String handle, String message, Location optionalLocation) throws CompileException {
|
|
if (this.optionalWarningHandler != null) {
|
|
this.optionalWarningHandler.handleWarning(handle, message, optionalLocation);
|
|
}
|
|
}
|
|
}
|