diff --git a/src/main/java/org/perlonjava/parser/IdentifierParser.java b/src/main/java/org/perlonjava/parser/IdentifierParser.java index 1c3012e1..9cbe5238 100644 --- a/src/main/java/org/perlonjava/parser/IdentifierParser.java +++ b/src/main/java/org/perlonjava/parser/IdentifierParser.java @@ -453,6 +453,16 @@ public static String parseSubroutineIdentifier(Parser parser) { parser.tokenIndex++; token = parser.tokens.get(parser.tokenIndex); nextToken = parser.tokens.get(parser.tokenIndex + 1); + + // Validate that what follows :: is a valid identifier start + // Allow EOF or closing tokens for package names that end with :: + if (token.type != LexerTokenType.IDENTIFIER && token.type != LexerTokenType.NUMBER && + !token.text.equals("'") && !token.text.equals("::") && !token.text.equals("->") && + token.type != LexerTokenType.EOF && + !(token.type == LexerTokenType.OPERATOR && (token.text.equals("}") || token.text.equals(";") || token.text.equals("=") || token.text.equals(")")))) { + // Bad name after :: + parser.throwCleanError("Bad name after " + variableName.toString() + "::"); + } continue; } @@ -475,6 +485,9 @@ public static String parseSubroutineIdentifier(Parser parser) { token = parser.tokens.get(parser.tokenIndex); nextToken = parser.tokens.get(parser.tokenIndex + 1); continue; + } else { + // Bad name after ' + parser.throwCleanError("Bad name after " + variableName.toString() + "'"); } } diff --git a/src/main/java/org/perlonjava/parser/ParsePrimary.java b/src/main/java/org/perlonjava/parser/ParsePrimary.java index 5ae398c3..f778d5e9 100644 --- a/src/main/java/org/perlonjava/parser/ParsePrimary.java +++ b/src/main/java/org/perlonjava/parser/ParsePrimary.java @@ -370,9 +370,37 @@ static Node parseOperator(Parser parser, LexerToken token, String operator) { if (isValidFileTestOperator(testOp)) { return parseFileTestOperator(parser, nextToken, operand); } else { - // For now, always treat invalid filetest operators as an error - // The function call case will be handled by the regular parsing logic - parser.throwError("Invalid filetest operator: -" + testOp); + // Not a valid filetest operator + // Check if there's a function with this name + String functionName = nextToken.text; + String fullName = parser.ctx.symbolTable.getCurrentPackage() + "::" + functionName; + RuntimeScalar codeRef = GlobalVariable.getGlobalCodeRef(fullName); + if (codeRef.getDefinedBoolean()) { + // There's a function with this name, treat as regular unary minus + // Don't do anything special here, just fall through to regular unary minus handling + } else { + // Not a valid filetest operator and no function with this name + // Check what comes after to decide how to handle + // Skip whitespace to find the next meaningful token + int afterIndex = parser.tokenIndex + 1; + while (afterIndex < parser.tokens.size() && + parser.tokens.get(afterIndex).type == LexerTokenType.WHITESPACE) { + afterIndex++; + } + if (afterIndex < parser.tokens.size()) { + LexerToken afterNext = parser.tokens.get(afterIndex); + // If there's something after the identifier that looks like an operand, + // it's probably an attempt to use a filetest operator, so give an error + if (afterNext.type == LexerTokenType.NUMBER || + afterNext.type == LexerTokenType.STRING || + afterNext.type == LexerTokenType.IDENTIFIER || + afterNext.text.equals("$") || afterNext.text.equals("@")) { + parser.throwError("syntax error - Invalid filetest operator near \"" + testOp + " 1\""); + } + } + // Otherwise, fall through to regular unary minus handling (will treat as string) + // This handles cases like -a in strict mode where it should be "-a" + } } } // Regular unary minus diff --git a/src/main/java/org/perlonjava/parser/StatementParser.java b/src/main/java/org/perlonjava/parser/StatementParser.java index 9ea0a1f6..fb09e18c 100644 --- a/src/main/java/org/perlonjava/parser/StatementParser.java +++ b/src/main/java/org/perlonjava/parser/StatementParser.java @@ -446,10 +446,14 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) { String fullName = null; String packageName = null; if (token.type != LexerTokenType.NUMBER && !token.text.matches("^v\\d+")) { + if (token.type != LexerTokenType.IDENTIFIER) { + // Not a valid module name token + throw new PerlCompilerException(parser.tokenIndex, "syntax error", parser.ctx.errorUtil); + } ctx.logDebug("use module: " + token); packageName = IdentifierParser.parseSubroutineIdentifier(parser); if (packageName == null) { - throw new PerlCompilerException(parser.tokenIndex, "Syntax error", parser.ctx.errorUtil); + throw new PerlCompilerException(parser.tokenIndex, "syntax error", parser.ctx.errorUtil); } fullName = NameNormalizer.moduleToFilename(packageName); ctx.logDebug("use fullName: " + fullName); diff --git a/src/main/java/org/perlonjava/parser/SubroutineParser.java b/src/main/java/org/perlonjava/parser/SubroutineParser.java index 0e5de4a0..bea01f5d 100644 --- a/src/main/java/org/perlonjava/parser/SubroutineParser.java +++ b/src/main/java/org/perlonjava/parser/SubroutineParser.java @@ -440,6 +440,18 @@ public static Node parseSubroutineDefinition(Parser parser, boolean wantName, St // Initialize a list to store any attributes the subroutine might have. List attributes = new ArrayList<>(); + + // Check for invalid prototype-like constructs without parentheses + if (peek(parser).text.equals("<") || peek(parser).text.equals("__FILE__")) { + // This looks like a prototype but without parentheses - it's invalid + if (subName != null) { + String fullName = NameNormalizer.normalizeVariableName(subName, parser.ctx.symbolTable.getCurrentPackage()); + parser.throwCleanError("Illegal declaration of subroutine " + fullName); + } else { + parser.throwCleanError("Illegal declaration of anonymous subroutine"); + } + } + // While there are attributes (denoted by a colon ':'), we keep parsing them. while (peek(parser).text.equals(":")) { prototype = consumeAttributes(parser, attributes); @@ -460,6 +472,16 @@ public static Node parseSubroutineDefinition(Parser parser, boolean wantName, St // If a prototype exists, we parse it using 'parseRawString' method which handles it like the 'q()' operator. // This means it will take everything inside the parentheses as a literal string. prototype = ((StringNode) StringParser.parseRawString(parser, "q")).value; + + // Validate prototype - certain characters are not allowed + if (prototype.contains("<>") || prototype.contains("__FILE__")) { + if (subName != null) { + String fullName = NameNormalizer.normalizeVariableName(subName, parser.ctx.symbolTable.getCurrentPackage()); + parser.throwCleanError("Illegal declaration of subroutine " + fullName); + } else { + parser.throwCleanError("Illegal declaration of anonymous subroutine"); + } + } // While there are attributes after the prototype (denoted by a colon ':'), we keep parsing them. while (peek(parser).text.equals(":")) { @@ -499,6 +521,11 @@ public static Node parseSubroutineDefinition(Parser parser, boolean wantName, St BlockNode block = ParseBlock.parseBlock(parser); // After the block, we expect a closing curly brace '}' to denote the end of the subroutine. + // Check if we reached EOF instead of finding the closing brace + if (parser.tokenIndex >= parser.tokens.size() || + parser.tokens.get(parser.tokenIndex).type == LexerTokenType.EOF) { + parser.throwCleanError("Missing right curly"); + } TokenUtils.consume(parser, LexerTokenType.OPERATOR, "}"); // Insert signature code in the block