From 7b4ee69f7bf3871e2e5ddfd47067a08ea69a3cff Mon Sep 17 00:00:00 2001 From: Mike Lee Date: Wed, 22 Jul 2026 14:05:44 +0800 Subject: [PATCH 01/13] Add ECS Linter and Formatter with initial linting rules - Introduced a new linter package (ecs_lint) that includes various linting rules for naming conventions, empty blocks, unreachable code, and formatting issues. - Implemented helper functions for checking naming styles (snake_case, UPPER_SNAKE_CASE) and identifying comments or blank lines. - Enhanced the parser (ecs_parser) to support format-specific syntax and lexical rules. - Created a test file (test_lint.ecs) containing intentional style issues to verify the linter's functionality. --- csbuild/ecs_bootstrap.json | 2 + csbuild/ecs_format.json | 14 + csbuild/ecs_lint.json | 13 + imports/ecs_bootstrap.csp | 43 +- imports/ecs_format.csp | 996 +++++++++++++++++++++++++++++++++++++ imports/ecs_lint.csp | 745 +++++++++++++++++++++++++++ imports/ecs_parser.csp | 394 +++++++++++++++ unit_tests/test_lint.ecs | 38 ++ 8 files changed, 2244 insertions(+), 1 deletion(-) create mode 100644 csbuild/ecs_format.json create mode 100644 csbuild/ecs_lint.json create mode 100644 imports/ecs_format.csp create mode 100644 imports/ecs_lint.csp create mode 100644 unit_tests/test_lint.ecs diff --git a/csbuild/ecs_bootstrap.json b/csbuild/ecs_bootstrap.json index ef55207..bd71798 100644 --- a/csbuild/ecs_bootstrap.json +++ b/csbuild/ecs_bootstrap.json @@ -9,6 +9,8 @@ "parsergen", "ecs_parser", "ecs_generator", + "ecs_lint", + "ecs_format", "sdk_extension", "codec", "regex" diff --git a/csbuild/ecs_format.json b/csbuild/ecs_format.json new file mode 100644 index 0000000..aa799fa --- /dev/null +++ b/csbuild/ecs_format.json @@ -0,0 +1,14 @@ +{ + "Type": "Package", + "Name": "ecs_format", + "Info": "Extended CovScript(ECS Lang) Formatter", + "Author": "Michael Lee", + "Version": "1.0.0", + "Target": "imports/ecs_format.csp", + "Dependencies": [ + "parsergen", + "regex", + "ecs_parser", + "ecs_lint" + ] +} diff --git a/csbuild/ecs_lint.json b/csbuild/ecs_lint.json new file mode 100644 index 0000000..ea85b8d --- /dev/null +++ b/csbuild/ecs_lint.json @@ -0,0 +1,13 @@ +{ + "Type": "Package", + "Name": "ecs_lint", + "Info": "Extended CovScript(ECS Lang) Linter & Formatter", + "Author": "Michael Lee", + "Version": "1.0.0", + "Target": "imports/ecs_lint.csp", + "Dependencies": [ + "parsergen", + "regex", + "ecs_parser" + ] +} diff --git a/imports/ecs_bootstrap.csp b/imports/ecs_bootstrap.csp index d73cade..3ead3da 100644 --- a/imports/ecs_bootstrap.csp +++ b/imports/ecs_bootstrap.csp @@ -20,7 +20,7 @@ package ecs_bootstrap -import parsergen, ecs_parser, ecs_generator, codec, regex +import parsergen, ecs_parser, ecs_generator, ecs_lint, ecs_format, codec, regex import sdk_extension as sdk var wrapper_ver = "1.7.0" @@ -155,6 +155,8 @@ function show_help() " -f Disable compile cache\n" + " -m Disable beautify\n" + " -c Check grammar only\n" + + " -l Lint check\n" + + " -F Format source file\n" + " -g Generate cSYM info\n" + " -d Run debugger\n" + " -o Set output path\n" + @@ -189,6 +191,8 @@ var splash = null var output = null var csym = false var repl = false +var lint_only = false +var format_only = false function process_args(cmd_args) var index = 1 @@ -214,6 +218,14 @@ function process_args(cmd_args) case "-c" no_run = true end + case "-l" + lint_only = true + no_run = true + end + case "-F" + format_only = true + no_run = true + end case "-s" silent = true end @@ -384,6 +396,8 @@ function run_ecs(cmd_args) output = null csym = false repl = false + lint_only = false + format_only = false exit_code = -1 process_args(cmd_args) @@ -438,6 +452,33 @@ function run_ecs(cmd_args) return exit_code end end + # Lint-only mode (uses main grammar, no codegen) + if lint_only + parser.add_grammar("ecs-lang", ecs_parser.grammar) + parser.from_file(file_name) + if parser.ast != null + var result = ecs_lint.lint_file(file_name) + if result != null + result[0].print_report() + end + end + return 0 + end + # Format mode (uses format-specific grammar with com tokens) + if format_only + var cvt = null + if unicode != null + cvt = parser.unicode_cvt + end + var output = ecs_format.format_file(file_name, cvt) + if output == null + return 1 + end + var ofs = iostream.ofstream(file_name) + ofs.print(output) + system.out.println(file_name + ": formatted.") + return 0 + end parser.add_grammar("ecs-lang", ecs_parser.grammar) parser.from_file(file_name) if parser.ast != null diff --git a/imports/ecs_format.csp b/imports/ecs_format.csp new file mode 100644 index 0000000..5923753 --- /dev/null +++ b/imports/ecs_format.csp @@ -0,0 +1,996 @@ +# ECS Formatter v1.0.0 +# +# 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. +# +# Copyright (C) 2017-2026 Michael Lee(李登淳) +# +# Email: mikecovlee@163.com +# Github: https://github.com/mikecovlee +# Website: http://covscript.org.cn + +package ecs_format + +import parsergen, ecs_parser, ecs_lint, regex + +# ============================================================ +# Format Writer +# ============================================================ + +class format_writer + var output = "" + var indent_size = 4 + var indent_level = 0 + var at_line_start = true + var need_space = false + var continuation = 0 + var last_text = "" + var force_space = false + + # Returns true if text starts like an identifier (letter / underscore / CJK) + function is_ident(text) + if text.empty() + return false + end + var c = to_integer(text[0]) + # A-Z, a-z, _, CJK Unified Ideographs U+4E00..U+9FFF + return (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c == 95 || (c >= 19968 && c <= 40959) + end + + function write(text) + if at_line_start + # Closing brackets align to base indent, not continuation + if text == "}" || text == ")" || text == "]" + continuation = 0 + end + for i = 0, i < (indent_level + continuation) * indent_size, ++i + output += ' ' + end + at_line_start = false + need_space = false + force_space = false + continuation = 0 + last_text = "" + end + if need_space || force_space + var suppress = false + if !force_space + # Never space before punctuation / closers + if text == "," || text == ";" || text == "." || text == "->" || text == "::" || text == ")" || text == "]" || text == "}" + suppress = true + end + # Space before ( [ only in non-call context (after operators / keywords) + if !suppress && (text == "(" || text == "[") + if last_text == ")" || last_text == "]" + suppress = true + else + if this.is_ident(last_text) + suppress = true + end + end + end + end + if !suppress + output += ' ' + end + need_space = false + force_space = false + end + output += text + last_text = text + # Operators / keywords set need_space; openers / accessors don't + if text == "(" || text == "[" || text == "{" || text == "." || text == "->" || text == "::" || text == "!" + need_space = false + else + need_space = true + end + end + + function newline() + output += '\n' + at_line_start = true + need_space = false + force_space = false + end + + function space() + need_space = true + end + + function push_indent() + ++indent_level + end + + function pop_indent() + --indent_level + if indent_level < 0 + indent_level = 0 + end + end + + function token(tok) + if tok.type == "endl" || tok.type == "com" + return + end + this.write(tok.data) + end + + function keyword(kw) + this.write(kw) + this.force_space = true + end +end + +# ============================================================ +# Format Visitor +# ============================================================ + +class format_visitor + var w = null + var ctx = null + + function run(file_name, code_buff, ast) + this.ctx = new ecs_lint.lint_context + this.ctx.file_name = file_name + this.ctx.code_buff = code_buff + this.w = new format_writer + # Walk AST (comments are now first-class "com" tokens) + this.visit_begin(ast.nodes) + return this.w.output + end + + # === Generic walker for expression children === + + function visit_children(nodes) + for idx = 0, idx < nodes.size, ++idx + var node = nodes[idx] + if typeid node == typeid parsergen.token_type + if node.type == "com" + # Standalone comment inside expression (edge case) + this.w.write(node.data) + this.w.newline() + end + if node.type != "endl" + this.w.token(node) + end + end + if typeid node == typeid parsergen.syntax_tree + this.dispatch(node) + end + end + end + + function dispatch(node) + var root = node.root + block + var matched = false + if !matched && root == "begin" + matched = true + this.visit_begin(node.nodes) + end + if !matched && root == "stmts" + matched = true + this.visit_stmts(node.nodes) + end + if !matched && root == "decl-stmts" + matched = true + this.visit_decl_stmts(node.nodes) + end + if !matched && root == "statement" + matched = true + this.visit_statement(node.nodes) + end + if !matched && root == "declaration" + matched = true + this.visit_declaration(node.nodes) + end + if !matched && root == "if-stmt" + matched = true + this.visit_if_stmt(node.nodes) + end + if !matched && root == "while-stmt" + matched = true + this.visit_while_stmt(node.nodes) + end + if !matched && root == "loop-stmt" + matched = true + this.visit_loop_stmt(node.nodes) + end + if !matched && root == "for-stmt" + matched = true + this.visit_for_stmt(node.nodes) + end + if !matched && root == "foreach-stmt" + matched = true + this.visit_foreach_stmt(node.nodes) + end + if !matched && root == "switch-stmt" + matched = true + this.visit_switch_stmt(node.nodes) + end + if !matched && root == "try-stmt" + matched = true + this.visit_try_stmt(node.nodes) + end + if !matched && root == "block-stmt" + matched = true + this.visit_block_stmt(node.nodes) + end + if !matched && root == "function-stmt" + matched = true + this.visit_function_stmt(node.nodes) + end + if !matched && root == "async-function-stmt" + matched = true + this.visit_async_function_stmt(node.nodes) + end + if !matched && root == "class-stmt" + matched = true + this.visit_class_stmt(node.nodes) + end + if !matched && root == "namespace-stmt" + matched = true + this.visit_namespace_stmt(node.nodes) + end + if !matched && root == "var-stmt" + matched = true + this.visit_var_stmt(node.nodes) + end + if !matched && root == "return-stmt" + matched = true + this.visit_return_stmt(node.nodes) + end + if !matched && root == "throw-stmt" + matched = true + this.visit_throw_stmt(node.nodes) + end + if !matched && root == "yield-stmt" + matched = true + this.visit_yield_stmt(node.nodes) + end + if !matched && root == "control-stmt" + matched = true + this.visit_control_stmt(node.nodes) + end + if !matched && root == "expr-stmt" + matched = true + this.visit_expr_stmt(node.nodes) + end + if !matched && root == "import-stmt" + matched = true + this.visit_import_stmt(node.nodes) + end + if !matched && root == "package-stmt" + matched = true + this.visit_package_stmt(node.nodes) + end + if !matched && root == "using-stmt" + matched = true + this.visit_using_stmt(node.nodes) + end + if !matched && root == "prep-stmt" + matched = true + this.visit_prep_stmt(node.nodes) + end + if !matched && root == "function-body" + matched = true + this.visit_function_body(node.nodes) + end + if !matched && root == "for-body" + matched = true + this.visit_for_body(node.nodes) + end + if !matched && root == "endline" + matched = true + this.visit_endline(node.nodes) + end + if !matched && root == "else-stmt" + matched = true + this.visit_else_stmt(node.nodes) + end + if !matched && root == "catch-stmt" + matched = true + this.visit_catch_stmt(node.nodes) + end + if !matched && root == "switch-case" + matched = true + this.visit_switch_case(node.nodes) + end + if !matched && root == "switch-default" + matched = true + this.visit_switch_default(node.nodes) + end + if !matched && root == "switch-stmts" + matched = true + this.visit_switch_stmts(node.nodes) + end + if !matched && root == "until-stmt" + matched = true + this.visit_until_stmt(node.nodes) + end + # Format-grammar helpers: eol = optional inline comment + newline + if !matched && root == "eol" + matched = true + this.visit_eol(node.nodes) + end + # Format-grammar helpers: eos = newline or standalone comment + if !matched && root == "eos" + matched = true + this.visit_eos(node.nodes) + end + if !matched && root == "nl" + matched = true + this.visit_nl(node.nodes) + end + # Unary operators: no space between operator and operand + if !matched && root == "unary-op" + matched = true + this.w.write(node.nodes[0].data) + this.w.need_space = false + end + # Lambda body: { stmts } or -> expr + if !matched && root == "lambda-body" + matched = true + this.visit_lambda_body(node.nodes) + end + if !matched + this.visit_children(node.nodes) + end + end + end + + # === Comment-aware line-end handlers === + + function visit_eol(nodes) + # eol = optional(com) + endl + # Inline comment (if present) before the newline + for idx = 0, idx < nodes.size, ++idx + var node = nodes[idx] + if typeid node == typeid parsergen.token_type + if node.type == "com" + this.w.space() + this.w.write(node.data) + end + if node.type == "endl" + this.w.newline() + end + end + end + end + + function visit_eos(nodes) + # eos = com + endl (standalone comment line) | endl (blank line) + if nodes.size >= 2 && typeid nodes[0] == typeid parsergen.token_type && nodes[0].type == "com" + # Standalone comment line + this.w.write(nodes[0].data) + this.w.newline() + else + # Blank line + this.w.newline() + end + end + + function visit_nl(nodes) + # nl = repeat(endl) — newlines within multi-line expressions + if nodes.size > 0 + this.w.continuation = 1 + this.w.newline() + end + end + + # === Top-level === + + function visit_begin(nodes) + # stmts is the first/only child of begin + for idx = 0, idx < nodes[0].nodes.size, ++idx + var node = nodes[0].nodes[idx] + if typeid node == typeid parsergen.syntax_tree + if node.root == "statement" + this.visit_statement(node.nodes) + end + if node.root == "eos" + this.visit_eos(node.nodes) + end + end + if typeid node == typeid parsergen.token_type + if node.type == "com" + this.w.write(node.data) + this.w.newline() + end + end + end + end + + function visit_stmts(nodes) + this.w.push_indent() + for idx = 0, idx < nodes.size, ++idx + var node = nodes[idx] + if typeid node == typeid parsergen.syntax_tree + if node.root == "statement" + this.visit_statement(node.nodes) + end + if node.root == "eos" + this.visit_eos(node.nodes) + end + if node.root == "eol" + this.visit_eol(node.nodes) + end + end + if typeid node == typeid parsergen.token_type + if node.type == "com" + this.w.write(node.data) + this.w.newline() + end + end + end + this.w.pop_indent() + end + + function visit_decl_stmts(nodes) + this.w.push_indent() + for idx = 0, idx < nodes.size, ++idx + var node = nodes[idx] + if typeid node == typeid parsergen.syntax_tree + if node.root == "declaration" + this.visit_declaration(node.nodes) + end + if node.root == "eos" + this.visit_eos(node.nodes) + end + if node.root == "eol" + this.visit_eol(node.nodes) + end + end + if typeid node == typeid parsergen.token_type + if node.type == "com" + this.w.write(node.data) + this.w.newline() + end + end + end + this.w.pop_indent() + end + + function visit_statement(nodes) + var node = nodes[0] + if typeid node == typeid parsergen.syntax_tree + if node.root == "if-stmt" || node.root == "while-stmt" || node.root == "for-stmt" || node.root == "foreach-stmt" || node.root == "loop-stmt" || node.root == "function-stmt" || node.root == "async-function-stmt" || node.root == "try-stmt" + ecs_lint.rule_empty_block(this.ctx, node.nodes) + end + this.dispatch(node) + end + end + + function visit_declaration(nodes) + var node = nodes[0] + if typeid node == typeid parsergen.syntax_tree + if node.root == "function-stmt" || node.root == "async-function-stmt" || node.root == "class-stmt" + ecs_lint.rule_empty_block(this.ctx, node.nodes) + end + this.dispatch(node) + end + end + + function visit_endline(nodes) + # endline = eol | ";" + # Delegate to dispatch for eol handling + for idx = 0, idx < nodes.size, ++idx + var node = nodes[idx] + if typeid node == typeid parsergen.syntax_tree + this.dispatch(node) + end + if typeid node == typeid parsergen.token_type + if node.data == ";" + this.w.write(";") + this.w.newline() + end + end + end + end + + # === Block statements === + + function visit_if_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("if") + this.w.space() + this.visit_children(nodes[idx++].nodes) + # eol (was endl token) + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + this.visit_stmts(nodes[idx++].nodes) + while idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "else-stmt" + this.visit_else_stmt(nodes[idx++].nodes) + this.visit_stmts(nodes[idx++].nodes) + end + ++idx; this.w.keyword("end") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + end + + function visit_else_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("else") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "if" + this.w.space() + ++idx; this.w.keyword("if") + this.w.space() + this.visit_children(nodes[idx++].nodes) + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + end + + function visit_while_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("while") + this.w.space() + this.visit_children(nodes[idx++].nodes) + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + this.visit_stmts(nodes[idx++].nodes) + ++idx; this.w.keyword("end") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + end + + function visit_loop_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("loop") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + this.visit_stmts(nodes[idx++].nodes) + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "until-stmt" + this.visit_until_stmt(nodes[idx++].nodes) + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "end" + ++idx; this.w.keyword("end") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + end + end + + function visit_until_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("until") + this.w.space() + this.visit_children(nodes[idx++].nodes) + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + end + + function visit_for_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("for") + this.w.space() + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "var-def" + this.visit_children(nodes[idx++].nodes) + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type + this.w.write(nodes[idx++].data) + this.w.space() + end + # skip nl + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" + ++idx + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "basic-expr" + this.visit_children(nodes[idx++].nodes) + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type + this.w.write(nodes[idx++].data) + this.w.space() + end + # skip nl + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" + ++idx + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "basic-expr" + this.visit_children(nodes[idx++].nodes) + end + this.visit_for_body(nodes[idx++].nodes) + end + + function visit_foreach_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("foreach") + this.w.space() + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].type == "id" + this.w.write(nodes[idx++].data) + this.w.space() + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "in" + ++idx; this.w.keyword("in") + this.w.space() + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "basic-expr" + this.visit_children(nodes[idx++].nodes) + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "for-body" + this.visit_for_body(nodes[idx++].nodes) + end + end + + function visit_for_body(nodes) + var idx = 0 + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "do" + ++idx; this.w.write("do") + this.w.space() + this.visit_children(nodes[idx++].nodes) + this.visit_endline(nodes[idx++].nodes) + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + this.visit_stmts(nodes[idx++].nodes) + ++idx; this.w.keyword("end") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + end + end + + function visit_switch_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("switch") + this.w.space() + this.visit_children(nodes[idx++].nodes) + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "switch-stmts" + this.visit_switch_stmts(nodes[idx++].nodes) + end + ++idx; this.w.keyword("end") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + end + + function visit_switch_stmts(nodes) + for idx = 0, idx < nodes.size, ++idx + var node = nodes[idx] + if typeid node == typeid parsergen.syntax_tree + if node.root == "switch-case" + this.visit_switch_case(node.nodes) + end + if node.root == "switch-default" + this.visit_switch_default(node.nodes) + end + if node.root == "eos" + this.visit_eos(node.nodes) + end + end + end + end + + function visit_switch_case(nodes) + var idx = 0 + ++idx; this.w.keyword("case") + this.w.space() + this.visit_children(nodes[idx++].nodes) + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + this.visit_stmts(nodes[idx++].nodes) + ++idx; this.w.keyword("end") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + end + + function visit_switch_default(nodes) + var idx = 0 + ++idx; this.w.keyword("default") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + this.visit_stmts(nodes[idx++].nodes) + ++idx; this.w.keyword("end") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + end + + function visit_try_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("try") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + this.visit_stmts(nodes[idx++].nodes) + while idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "catch-stmt" + this.visit_catch_stmt(nodes[idx++].nodes) + this.visit_stmts(nodes[idx++].nodes) + end + ++idx; this.w.keyword("end") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + end + + function visit_catch_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("catch") + this.w.space() + this.w.write(nodes[idx++].data) + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == ":" + this.w.write(nodes[idx++].data) + this.w.space() + this.visit_children(nodes[idx++].nodes) + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + end + + function visit_block_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("block") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + this.visit_stmts(nodes[idx++].nodes) + ++idx; this.w.keyword("end") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + end + + # === Declarations === + + function visit_function_stmt(nodes) + ecs_lint.rule_function_naming(this.ctx, nodes) + var idx = 0 + ++idx; this.w.keyword("function") + this.w.space() + this.w.write(nodes[idx++].data) + ++idx; this.w.write("(") + # skip nl, handle argument-list, skip nl + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" + ++idx + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "argument-list" + this.visit_children(nodes[idx++].nodes) + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" + ++idx + end + ++idx; this.w.write(")") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "override" + this.w.space() + ++idx; this.w.write("override") + end + this.visit_function_body(nodes[idx++].nodes) + end + + function visit_async_function_stmt(nodes) + ecs_lint.rule_async_function_naming(this.ctx, nodes) + var idx = 0 + ++idx; this.w.keyword("async") + this.w.space() + ++idx; this.w.keyword("function") + this.w.space() + this.w.write(nodes[idx++].data) + ++idx; this.w.write("(") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" + ++idx + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "argument-list" + this.visit_children(nodes[idx++].nodes) + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" + ++idx + end + ++idx; this.w.write(")") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "override" + this.w.space() + ++idx; this.w.write("override") + end + this.visit_function_body(nodes[idx++].nodes) + end + + function visit_function_body(nodes) + var idx = 0 + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "{" + ++idx; this.w.write("{") + this.w.space() + this.visit_stmts(nodes[idx++].nodes) + ++idx; this.w.write("}") + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + this.visit_stmts(nodes[idx++].nodes) + ++idx; this.w.keyword("end") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + end + end + + function visit_lambda_body(nodes) + var idx = 0 + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "{" + ++idx; this.w.write("{") + this.visit_stmts(nodes[idx++].nodes) + ++idx; this.w.write("}") + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "->" + ++idx; this.w.write("->") + this.w.space() + this.visit_children(nodes[idx++].nodes) + end + end + + function visit_class_stmt(nodes) + ecs_lint.rule_class_naming(this.ctx, nodes) + var idx = 0 + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type + this.w.keyword(nodes[idx++].data) + end + this.w.space() + this.w.write(nodes[idx++].data) + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "extends" + this.w.space() + ++idx; this.w.keyword("extends") + this.w.space() + this.visit_children(nodes[idx++].nodes) + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + this.visit_decl_stmts(nodes[idx++].nodes) + ++idx; this.w.keyword("end") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + end + + function visit_namespace_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("namespace") + this.w.space() + this.w.write(nodes[idx++].data) + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + this.visit_decl_stmts(nodes[idx++].nodes) + ++idx; this.w.keyword("end") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" + this.visit_eol(nodes[idx++].nodes) + end + end + + function visit_var_stmt(nodes) + ecs_lint.rule_variable_naming(this.ctx, nodes) + var idx = 0 + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type + this.w.keyword(nodes[idx++].data) + end + this.w.space() + this.visit_children(nodes[idx++].nodes) + this.visit_endline(nodes[idx++].nodes) + end + + # === Simple statements === + + function visit_return_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("return") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root != "endline" + this.w.space() + this.visit_children(nodes[idx++].nodes) + end + this.visit_endline(nodes[idx++].nodes) + end + + function visit_throw_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("throw") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root != "endline" + this.w.space() + this.visit_children(nodes[idx++].nodes) + end + this.visit_endline(nodes[idx++].nodes) + end + + function visit_yield_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("yield") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root != "endline" + this.w.space() + this.visit_children(nodes[idx++].nodes) + end + this.visit_endline(nodes[idx++].nodes) + end + + function visit_control_stmt(nodes) + var idx = 0 + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type + this.w.keyword(nodes[idx++].data) + end + this.visit_endline(nodes[idx++].nodes) + end + + function visit_expr_stmt(nodes) + var idx = 0 + ecs_lint.rule_useless_expression(this.ctx, nodes) + this.visit_children(nodes[idx++].nodes) + this.visit_endline(nodes[idx++].nodes) + end + + function visit_import_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("import") + this.w.space() + this.visit_children(nodes[idx++].nodes) + this.visit_endline(nodes[idx++].nodes) + end + + function visit_package_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("package") + this.w.space() + this.w.write(nodes[idx++].data) + this.visit_endline(nodes[idx++].nodes) + end + + function visit_using_stmt(nodes) + var idx = 0 + ++idx; this.w.keyword("using") + this.w.space() + this.visit_children(nodes[idx++].nodes) + this.visit_endline(nodes[idx++].nodes) + end + + function visit_prep_stmt(nodes) + for idx = 0, idx < nodes.size, ++idx + if typeid nodes[idx] == typeid parsergen.token_type + this.w.write(nodes[idx].data) + end + end + this.w.newline() + end +end + +# ============================================================ +# Public API +# ============================================================ + +function format_ast(file_name, code_buff, ast) + var vis = new format_visitor + return vis.run(file_name, code_buff, ast) +end + +function format_file(file_name, unicode_cvt) + # Use format-specific grammar so comments appear as "com" tokens in AST + var parser = new parsergen.generator + if unicode_cvt != null + parser.unicode_cvt = unicode_cvt + end + parser.add_grammar("ecs-lang-fmt", ecs_parser.get_fmt_grammar()) + parser.from_file(file_name) + if parser.ast == null + system.out.println("Error: Failed to parse file '" + file_name + "'.") + return null + end + return format_ast(file_name, parser.code_buff, parser.ast) +end + +var version = "1.0.0" diff --git a/imports/ecs_lint.csp b/imports/ecs_lint.csp new file mode 100644 index 0000000..117cdae --- /dev/null +++ b/imports/ecs_lint.csp @@ -0,0 +1,745 @@ +# ECS Linter & Formatter v1.0.0 +# +# 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. +# +# Copyright (C) 2017-2026 Michael Lee(李登淳) +# +# Email: mikecovlee@163.com +# Github: https://github.com/mikecovlee +# Website: http://covscript.org.cn + +package ecs_lint + +import parsergen, ecs_parser, regex + +# ============================================================ +# Severity Levels +# ============================================================ + +constant SEVERITY_ERROR = "error" +constant SEVERITY_WARNING = "warning" +constant SEVERITY_INFO = "info" + +# ============================================================ +# Issue +# ============================================================ + +struct issue + var pos = {0, 0} + var message = "" + var severity = "" + var rule_name = "" +end + +# ============================================================ +# Lint Context +# ============================================================ + +struct lint_context + var file_name = "" + var code_buff = new array + var issues = new array + var indent_info = new hash_map + var indent_level = 0 + + function add_issue(pos, message, severity, rule_name) + var iss = new issue + iss.pos = pos + iss.message = message + iss.severity = severity + iss.rule_name = rule_name + this.issues.push_back(iss) + end +end + +# ============================================================ +# Helpers +# ============================================================ + +function get_pos(nodes) + # Walk into nested syntax trees to find the first token + var current = nodes + while !current.empty() && typeid current[0] == typeid parsergen.syntax_tree + current = current[0].nodes + end + if !current.empty() && typeid current[0] == typeid parsergen.token_type + return current[0].pos + else + return {0, 0} + end +end + +function is_snake_case(name) + for i = 0, i < name.size, ++i + var code = to_integer(name[i]) + if code >= 65 && code <= 90 + return false + end + end + return true +end + +function is_upper_snake_case(name) + for i = 0, i < name.size, ++i + var code = to_integer(name[i]) + if code >= 97 && code <= 122 + return false + end + end + return true +end + +function is_comment_or_blank_line(line) + # Check if line is blank or comment-only (first non-space char is #) + for i = 0, i < line.size, ++i + var ch = line[i] + if ch == '#' + return true # comment line + end + if ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n' + return false # has non-space, non-comment content + end + end + return true # blank line +end + +# ============================================================ +# Lint Rules +# ============================================================ + +function rule_function_naming(ctx, nodes) + # nodes: function-stmt children + # Pattern: "function" id "(" ... ")" ... + var idx = 0 + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "function" + ++idx + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].type == "id" + var name = nodes[idx].data + if !is_snake_case(name) + ctx.add_issue(nodes[idx].pos, "Function name '" + name + "' should be snake_case.", SEVERITY_WARNING, "function_naming") + end + end +end + +function rule_async_function_naming(ctx, nodes) + # nodes: async-function-stmt children + # Pattern: "async" "function" id "(" ... ")" ... + var idx = 0 + while idx < nodes.size + if typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].type == "id" + var name = nodes[idx].data + if !is_snake_case(name) + ctx.add_issue(nodes[idx].pos, "Function name '" + name + "' should be snake_case.", SEVERITY_WARNING, "function_naming") + end + break + end + ++idx + end +end + +function rule_variable_naming(ctx, nodes) + # nodes: var-stmt children + # Pattern: ("var"|"link"|"constant") (id ... | var-list | var-bind) + var idx = 0 + var decl_kind = "" + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type + decl_kind = nodes[idx].data + ++idx + end + # var-def + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "var-def" + var def_nodes = nodes[idx].nodes + if decl_kind == "constant" + # Check constant naming + check_var_names(ctx, def_nodes, true) + else + # Check var/link naming + check_var_names(ctx, def_nodes, false) + end + end +end + +function check_var_names(ctx, def_nodes, is_constant) + var didx = 0 + # var-def is either var-bind = expr OR var-list + if didx < def_nodes.size && typeid def_nodes[didx] == typeid parsergen.syntax_tree + if def_nodes[didx].root == "var-bind" + # Skip bind patterns for now — too complex to check naming + return + end + if def_nodes[didx].root == "var-list" + check_var_list(ctx, def_nodes[didx].nodes, is_constant) + end + end +end + +function check_var_list(ctx, nodes, is_constant) + var idx = 0 + while idx < nodes.size + if typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].type == "id" + var name = nodes[idx].data + if is_constant + if !is_upper_snake_case(name) + ctx.add_issue(nodes[idx].pos, "Constant name '" + name + "' should be UPPER_SNAKE_CASE.", SEVERITY_WARNING, "constant_naming") + end + else + if !is_snake_case(name) + ctx.add_issue(nodes[idx].pos, "Variable name '" + name + "' should be snake_case.", SEVERITY_WARNING, "variable_naming") + end + end + end + # Skip to next comma or end + ++idx + while idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree + ++idx + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "," + ++idx + end + end +end + +function rule_class_naming(ctx, nodes) + # nodes: class-stmt children + # Pattern: ("class"|"struct") id ... + var idx = 0 + # skip "class" or "struct" + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type + ++idx + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].type == "id" + var name = nodes[idx].data + if !is_snake_case(name) + ctx.add_issue(nodes[idx].pos, "Class/struct name '" + name + "' should be snake_case.", SEVERITY_WARNING, "class_naming") + end + end +end + +function rule_empty_block(ctx, nodes) + # Extract the first meaningful token's position and data + var first_pos = {0, 0} + var keyword_name = "block" + for i = 0, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.token_type + if first_pos[1] == 0 + first_pos = nodes[i].pos + end + if keyword_name == "block" && nodes[i].data != "endl" + keyword_name = nodes[i].data + end + end + if typeid nodes[i] == typeid parsergen.syntax_tree && nodes[i].root == "stmts" + var stmts_nodes = nodes[i].nodes + var has_stmt = false + for j = 0, j < stmts_nodes.size, ++j + if typeid stmts_nodes[j] == typeid parsergen.syntax_tree && stmts_nodes[j].root == "statement" + has_stmt = true + break + end + end + if !has_stmt + ctx.add_issue(first_pos, "Empty " + keyword_name + " block body.", SEVERITY_WARNING, "empty_block") + end + break + end + end +end + +function rule_unreachable_code(ctx, nodes) + # Check if there are statements after return/throw/break/continue + # within the same stmts sequence + var seen_terminator = false + for i = 0, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree && nodes[i].root == "statement" + var stmt_nodes = nodes[i].nodes + if !stmt_nodes.empty() + var first = stmt_nodes[0] + if typeid first == typeid parsergen.syntax_tree + if first.root == "return-stmt" || first.root == "throw-stmt" || first.root == "control-stmt" + seen_terminator = true + else + if seen_terminator + ctx.add_issue(get_pos(stmt_nodes), "Unreachable code after return, throw, break or continue.", SEVERITY_ERROR, "unreachable_code") + end + end + end + end + end + end +end + +function rule_useless_expression(ctx, nodes) + # TODO: Detect expressions whose results are discarded + # Requires deep analysis of the expression chain to distinguish + # between calls with side effects and pure expressions. + # For now, skip this check. +end + +# === Source-level rules (operate on code_buff, not AST) === + +function rule_trailing_whitespace(ctx) + for i = 0, i < ctx.code_buff.size, ++i + var line = ctx.code_buff[i] + var line_len = line.size + # Remove trailing \n for check + if line_len > 0 && line[line_len - 1] == '\n' + --line_len + end + if line_len > 0 && line[line_len - 1] == '\r' + --line_len + end + if line_len > 0 && (line[line_len - 1] == ' ' || line[line_len - 1] == '\t') + ctx.add_issue({line_len, i}, "Trailing whitespace.", SEVERITY_INFO, "trailing_whitespace") + end + end +end + +function rule_consecutive_blank_lines(ctx) + var blank_count = 0 + for i = 0, i < ctx.code_buff.size, ++i + var line = ctx.code_buff[i] + var is_blank = true + for j = 0, j < line.size, ++j + var ch = line[j] + if ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n' + is_blank = false + break + end + end + if is_blank + ++blank_count + else + if blank_count > 2 + ctx.add_issue({1, i - blank_count + 2}, "Too many consecutive blank lines (" + to_string(blank_count) + ").", SEVERITY_INFO, "consecutive_blank_lines") + end + blank_count = 0 + end + end +end + +function rule_indentation_consistency(ctx) + var has_tabs = false + var has_spaces = false + for i = 0, i < ctx.code_buff.size, ++i + var line = ctx.code_buff[i] + var in_indent = true + for j = 0, j < line.size, ++j + var ch = line[j] + if ch == '\t' + if in_indent + has_tabs = true + end + end + if ch == ' ' + if in_indent + has_spaces = true + end + end + if ch != '\r' && ch != '\n' + in_indent = false + end + end + end + if has_tabs && has_spaces + ctx.add_issue({1, 1}, "Mixed tabs and spaces in indentation.", SEVERITY_WARNING, "indentation_consistency") + end +end + +# ============================================================ +# Linter — AST Walker + Rule Engine +# ============================================================ + +class linter + var ctx = null + + function run(file_name, code_buff, ast) + this.ctx = new lint_context + this.ctx.file_name = file_name + this.ctx.code_buff = code_buff + # Source-level checks + rule_trailing_whitespace(this.ctx) + rule_consecutive_blank_lines(this.ctx) + rule_indentation_consistency(this.ctx) + # Walk AST + if !ast.nodes.empty() + this.visit_begin(ast.nodes) + end + return this.ctx.issues + end + + function print_report() + if this.ctx.issues.empty() + return + end + var errors = new array + foreach iss in this.ctx.issues + var err = new parsergen.lex_error + err.text = iss.severity + ": [" + iss.rule_name + "] " + iss.message + err.pos = iss.pos + errors.push_back(err) + end + parsergen.print_error(this.ctx.file_name, this.ctx.code_buff, errors) + end + + function count_by_severity(severity) + var count = 0 + foreach iss in this.ctx.issues + if iss.severity == severity + ++count + end + end + return count + end + + # === AST Dispatch === + + function visit_node(nodes) + if nodes.empty() + return + end + var idx = 0 + while idx < nodes.size + var node = nodes[idx] + if typeid node == typeid parsergen.syntax_tree + var root = node.root + # Dispatch to specific visitor + block + var matched = false + if !matched && root == "begin" + matched = true + this.visit_begin(node.nodes) + end + if !matched && root == "stmts" + matched = true + this.visit_stmts(node.nodes) + end + if !matched && root == "decl-stmts" + matched = true + this.visit_decl_stmts(node.nodes) + end + if !matched && root == "statement" + matched = true + this.visit_statement(node.nodes) + end + if !matched && root == "declaration" + matched = true + this.visit_declaration(node.nodes) + end + if !matched && root == "var-stmt" + matched = true + this.visit_var_stmt(node.nodes) + end + if !matched && root == "function-stmt" + matched = true + this.visit_function_stmt(node.nodes) + end + if !matched && root == "async-function-stmt" + matched = true + this.visit_async_function_stmt(node.nodes) + end + if !matched && root == "class-stmt" + matched = true + this.visit_class_stmt(node.nodes) + end + if !matched && root == "if-stmt" + matched = true + this.visit_if_stmt(node.nodes) + end + if !matched && root == "while-stmt" + matched = true + this.visit_while_stmt(node.nodes) + end + if !matched && root == "loop-stmt" + matched = true + this.visit_loop_stmt(node.nodes) + end + if !matched && root == "for-stmt" + matched = true + this.visit_for_stmt(node.nodes) + end + if !matched && root == "foreach-stmt" + matched = true + this.visit_foreach_stmt(node.nodes) + end + if !matched && root == "switch-stmt" + matched = true + this.visit_switch_stmt(node.nodes) + end + if !matched && root == "try-stmt" + matched = true + this.visit_try_stmt(node.nodes) + end + if !matched && root == "namespace-stmt" + matched = true + this.visit_namespace_stmt(node.nodes) + end + if !matched && root == "block-stmt" + matched = true + this.visit_block_stmt(node.nodes) + end + if !matched && root == "return-stmt" + matched = true + this.visit_return_stmt(node.nodes) + end + if !matched && root == "throw-stmt" + matched = true + this.visit_throw_stmt(node.nodes) + end + if !matched && root == "control-stmt" + matched = true + this.visit_control_stmt(node.nodes) + end + if !matched && root == "yield-stmt" + matched = true + this.visit_yield_stmt(node.nodes) + end + if !matched && root == "expr-stmt" + matched = true + this.visit_expr_stmt(node.nodes) + end + if !matched + # For expression-related nodes, just recurse into children + this.visit_node(node.nodes) + end + end + end + ++idx + end + end + + # === Top-level === + + function visit_begin(nodes) + this.visit_node(nodes) + end + + function visit_stmts(nodes) + this.ctx.indent_level += 1 + # Check unreachable code + rule_unreachable_code(this.ctx, nodes) + # Recurse into children + for i = 0, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree && nodes[i].root == "statement" + this.visit_statement(nodes[i].nodes) + end + end + this.ctx.indent_level -= 1 + end + + function visit_decl_stmts(nodes) + this.ctx.indent_level += 1 + for i = 0, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree && nodes[i].root == "declaration" + this.visit_declaration(nodes[i].nodes) + end + end + this.ctx.indent_level -= 1 + end + + function visit_statement(nodes) + this.visit_node(nodes) + end + + function visit_declaration(nodes) + this.visit_node(nodes) + end + + # === Declarations === + + function visit_var_stmt(nodes) + rule_variable_naming(this.ctx, nodes) + # Recurse into var-def and initializers + for i = 0, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree + this.visit_node(nodes[i].nodes) + end + end + end + + function visit_function_stmt(nodes) + rule_function_naming(this.ctx, nodes) + # Check empty body + rule_empty_block(this.ctx, nodes) + # Recurse into body + for i = 0, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree + if nodes[i].root == "stmts" || nodes[i].root == "function-body" + this.visit_node(nodes[i].nodes) + else + this.visit_node(nodes[i].nodes) + end + end + end + end + + function visit_async_function_stmt(nodes) + rule_async_function_naming(this.ctx, nodes) + rule_empty_block(this.ctx, nodes) + for i = 0, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree + if nodes[i].root == "stmts" || nodes[i].root == "function-body" + this.visit_node(nodes[i].nodes) + else + this.visit_node(nodes[i].nodes) + end + end + end + end + + function visit_class_stmt(nodes) + rule_class_naming(this.ctx, nodes) + for i = 0, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree + if nodes[i].root == "decl-stmts" + this.visit_decl_stmts(nodes[i].nodes) + else + this.visit_node(nodes[i].nodes) + end + end + end + end + + # === Control Flow === + + function visit_if_stmt(nodes) + rule_empty_block(this.ctx, nodes) + for i = 0, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree + if nodes[i].root == "stmts" + rule_unreachable_code(this.ctx, nodes[i].nodes) + end + this.visit_node(nodes[i].nodes) + end + end + end + + function visit_while_stmt(nodes) + rule_empty_block(this.ctx, nodes) + for i = 0, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree + if nodes[i].root == "stmts" + rule_unreachable_code(this.ctx, nodes[i].nodes) + end + this.visit_node(nodes[i].nodes) + end + end + end + + function visit_loop_stmt(nodes) + rule_empty_block(this.ctx, nodes) + for i = 0, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree + if nodes[i].root == "stmts" + rule_unreachable_code(this.ctx, nodes[i].nodes) + end + this.visit_node(nodes[i].nodes) + end + end + end + + function visit_for_stmt(nodes) + rule_empty_block(this.ctx, nodes) + this.visit_node(nodes) + end + + function visit_foreach_stmt(nodes) + rule_empty_block(this.ctx, nodes) + this.visit_node(nodes) + end + + function visit_switch_stmt(nodes) + this.visit_node(nodes) + end + + function visit_try_stmt(nodes) + # Check empty catch blocks + rule_empty_block(this.ctx, nodes) + for i = 0, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree + if nodes[i].root == "stmts" + rule_unreachable_code(this.ctx, nodes[i].nodes) + end + this.visit_node(nodes[i].nodes) + end + end + end + + # === Other statements === + + function visit_namespace_stmt(nodes) + for i = 0, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree + if nodes[i].root == "decl-stmts" + this.visit_decl_stmts(nodes[i].nodes) + else + this.visit_node(nodes[i].nodes) + end + end + end + end + + function visit_block_stmt(nodes) + for i = 0, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree + if nodes[i].root == "stmts" + rule_unreachable_code(this.ctx, nodes[i].nodes) + end + this.visit_node(nodes[i].nodes) + end + end + end + + function visit_return_stmt(nodes) + # For unreachable code detection, handled in visit_stmts + this.visit_node(nodes) + end + + function visit_throw_stmt(nodes) + this.visit_node(nodes) + end + + function visit_control_stmt(nodes) + # break/continue — for unreachable code detection + this.visit_node(nodes) + end + + function visit_yield_stmt(nodes) + this.visit_node(nodes) + end + + function visit_expr_stmt(nodes) + rule_useless_expression(this.ctx, nodes) + this.visit_node(nodes) + end +end + + +# ============================================================ +# Public API +# ============================================================ + +function lint_file(file_name) + var parser = new parsergen.generator + parser.add_grammar("ecs-lang", ecs_parser.grammar) + parser.from_file(file_name) + if parser.ast == null + system.out.println("Error: Failed to parse file '" + file_name + "'.") + return null + end + var lint = new linter + var issues = lint.run(file_name, parser.code_buff, parser.ast) + return {lint, issues} +end + +var version = "1.0.0" diff --git a/imports/ecs_parser.csp b/imports/ecs_parser.csp index e68a0ea..058bc0d 100644 --- a/imports/ecs_parser.csp +++ b/imports/ecs_parser.csp @@ -43,6 +43,13 @@ function get_lexical(reg_builder) @end end +function get_fmt_lexical(reg_builder) + var lex = get_lexical(reg_builder) + lex["ign"] = reg_builder("^[ \\f\\r\\t]+$") + lex["com"] = reg_builder("^#.*$") + return lex +end + @begin var covscript_syntax = { # Beginning of Parsing @@ -373,6 +380,393 @@ var covscript_syntax = { }.to_hash_map() @end +# ============================================================ +# Format-specific syntax — comments are explicit "com" tokens +# ============================================================ + +function get_fmt_syntax() + @begin + return { + # === Helper rules (NO ignore rule — all newlines are explicit) === + # nl: zero or more newlines (for multi-line expressions) + "nl" : {syntax.repeat(syntax.token("endl"))}, + # eol: end-of-statement-line = optional inline comment + newline + "eol" : {syntax.optional(syntax.token("com")), syntax.token("endl")}, + # eos: statement separator = standalone comment line OR blank line + "eos" : {syntax.cond_or( + {syntax.token("com"), syntax.token("endl")}, + {syntax.token("endl")} + )}, + # endline: eol or semicolon + "endline" : {syntax.cond_or( + {syntax.ref("eol")}, + {syntax.term(";")} + )}, + # === Bootstrap === + "begin" : { + syntax.ref("stmts") + }, + "stmts" : { + syntax.repeat(syntax.ref("eos")), + syntax.repeat(syntax.nlook(syntax.ref("endblock")), syntax.ref("statement"), syntax.repeat(syntax.ref("eos"))) + }, + "decl-stmts" : { + syntax.repeat(syntax.ref("eos")), + syntax.repeat(syntax.nlook(syntax.ref("endblock")), syntax.ref("declaration"), syntax.repeat(syntax.ref("eos"))) + }, + "endblock" : {syntax.cond_or( + {syntax.ref("end-stmt")}, + {syntax.ref("else-stmt")}, + {syntax.ref("until-stmt")}, + {syntax.ref("catch-stmt")} + )}, + "statement" : {syntax.cond_or( + {syntax.ref("prep-stmt")}, + {syntax.ref("package-stmt")}, + {syntax.ref("import-stmt")}, + {syntax.ref("var-stmt")}, + {syntax.ref("block-stmt")}, + {syntax.ref("namespace-stmt")}, + {syntax.ref("using-stmt")}, + {syntax.ref("if-stmt")}, + {syntax.ref("switch-stmt")}, + {syntax.ref("while-stmt")}, + {syntax.ref("loop-stmt")}, + {syntax.ref("for-stmt")}, + {syntax.ref("foreach-stmt")}, + {syntax.ref("async-function-stmt")}, + {syntax.ref("control-stmt")}, + {syntax.ref("function-stmt")}, + {syntax.ref("yield-stmt")}, + {syntax.ref("return-stmt")}, + {syntax.ref("try-stmt")}, + {syntax.ref("throw-stmt")}, + {syntax.ref("class-stmt")}, + {syntax.ref("expr-stmt")} + )}, + "declaration" : {syntax.cond_or( + {syntax.ref("prep-stmt")}, + {syntax.ref("namespace-stmt")}, + {syntax.ref("var-stmt")}, + {syntax.ref("using-stmt")}, + {syntax.ref("async-function-stmt")}, + {syntax.ref("function-stmt")}, + {syntax.ref("class-stmt")} + )}, + # === Simple statements === + "prep-stmt" : { + syntax.token("prep"), syntax.ref("eol") + }, + "package-stmt" : { + syntax.term("package"), syntax.token("id"), syntax.ref("endline") + }, + "import-stmt" : { + syntax.term("import"), syntax.ref("import-list"), syntax.ref("endline") + }, + "module-list" : { + syntax.token("id"), syntax.optional(syntax.term("."), syntax.cond_or({syntax.term("*")}, {syntax.ref("module-list")})) + }, + "import-list" : { + syntax.ref("module-list"), syntax.optional(syntax.term("as"), syntax.token("id")), syntax.optional(syntax.term(","), syntax.ref("import-list")) + }, + "var-def" : { + syntax.cond_or({syntax.ref("var-bind"), syntax.term("="), syntax.ref("nl"), syntax.ref("basic-expr")}, {syntax.ref("var-list")}) + }, + "var-stmt" : { + syntax.cond_or({syntax.term("var")}, {syntax.term("link")}, {syntax.term("constant")}), syntax.ref("var-def"), syntax.ref("endline") + }, + "var-bind" : { + syntax.term("("), syntax.ref("nl"), syntax.ref("var-bind-list"), syntax.repeat(syntax.term(","), syntax.ref("nl"), syntax.ref("var-bind-list")), syntax.ref("nl"), syntax.term(")") + }, + "var-bind-list" : {syntax.cond_or( + {syntax.token("id")}, + {syntax.term("...")}, + {syntax.ref("var-bind")} + )}, + "var-list" : { + syntax.token("id"), syntax.cond_or( + {syntax.term("="), syntax.ref("nl"), syntax.ref("basic-expr")}, + {syntax.term("as"), syntax.ref("unary-expr"), syntax.optional(syntax.ref("array"))} + ), syntax.optional(syntax.term(","), syntax.ref("nl"), syntax.ref("var-list")) + }, + "using-stmt" : { + syntax.term("using"), syntax.ref("using-list"), syntax.ref("endline") + }, + "using-list" : { + syntax.ref("module-list"), syntax.optional(syntax.term(","), syntax.ref("using-list")) + }, + "control-stmt" : { + syntax.cond_or({syntax.term("break")}, {syntax.term("continue")}), syntax.ref("endline") + }, + "expr-stmt" : { + syntax.ref("expr"), syntax.ref("endline") + }, + "return-stmt" : { + syntax.term("return"), syntax.optional(syntax.nlook(syntax.ref("eol")), syntax.ref("expr")), syntax.ref("endline") + }, + "throw-stmt" : { + syntax.term("throw"), syntax.optional(syntax.nlook(syntax.ref("eol")), syntax.ref("expr")), syntax.ref("endline") + }, + "yield-stmt" : { + syntax.term("yield"), syntax.optional(syntax.nlook(syntax.ref("eol")), syntax.ref("expr")), syntax.ref("endline") + }, + # === Block statements === + "block-stmt" : { + syntax.term("block"), syntax.ref("eol"), syntax.ref("stmts"), syntax.term("end"), syntax.ref("eol") + }, + "namespace-stmt" : { + syntax.term("namespace"), syntax.token("id"), syntax.ref("eol"), syntax.ref("decl-stmts"), syntax.term("end"), syntax.ref("eol") + }, + "if-stmt" : { + syntax.term("if"), syntax.ref("basic-expr"), syntax.ref("eol"), syntax.ref("stmts"), syntax.repeat(syntax.ref("else-stmt"), syntax.ref("stmts")), syntax.term("end"), syntax.ref("eol") + }, + "else-stmt" : { + syntax.term("else"), syntax.optional(syntax.nlook(syntax.ref("eol")), syntax.term("if"), syntax.ref("basic-expr")), syntax.ref("eol") + }, + "switch-stmt" : { + syntax.term("switch"), syntax.ref("basic-expr"), syntax.ref("eol"), syntax.ref("switch-stmts"), syntax.term("end"), syntax.ref("eol") + }, + "switch-stmts" : { + syntax.repeat(syntax.ref("eos")), + syntax.repeat(syntax.cond_or({syntax.ref("switch-case")}, {syntax.ref("switch-default")}), syntax.repeat(syntax.ref("eos"))) + }, + "switch-case" : { + syntax.term("case"), syntax.ref("logic-or-expr"), syntax.ref("eol"), syntax.ref("stmts"), syntax.term("end"), syntax.ref("eol") + }, + "switch-default" : { + syntax.term("default"), syntax.ref("eol"), syntax.ref("stmts"), syntax.term("end"), syntax.ref("eol") + }, + "while-stmt" : { + syntax.term("while"), syntax.ref("basic-expr"), syntax.ref("eol"), syntax.ref("stmts"), syntax.term("end"), syntax.ref("eol") + }, + "loop-stmt" : { + syntax.term("loop"), syntax.ref("eol"), syntax.ref("stmts"), syntax.cond_or({syntax.ref("until-stmt")}, {syntax.term("end"), syntax.ref("eol")}) + }, + "until-stmt" : { + syntax.term("until"), syntax.ref("basic-expr"), syntax.ref("eol") + }, + "for-stmt" : { + syntax.term("for"), syntax.optional(syntax.ref("var-def")), + syntax.cond_or({syntax.term(";")}, {syntax.term(",")}), syntax.ref("nl"), + syntax.optional(syntax.ref("basic-expr")), + syntax.cond_or({syntax.term(";")}, {syntax.term(",")}), syntax.ref("nl"), + syntax.optional(syntax.ref("basic-expr")), + syntax.ref("for-body") + }, + "foreach-stmt" : { + syntax.term("foreach"), syntax.optional(syntax.nlook(syntax.term("in")), syntax.token("id")), syntax.term("in"), syntax.ref("basic-expr"), syntax.ref("for-body") + }, + "for-body" : {syntax.cond_or( + {syntax.term("do"), syntax.ref("basic-expr"), syntax.ref("endline")}, + {syntax.ref("eol"), syntax.ref("stmts"), syntax.term("end"), syntax.ref("eol")} + )}, + "try-stmt" : { + syntax.term("try"), syntax.ref("eol"), syntax.ref("stmts"), syntax.repeat(syntax.ref("catch-stmt"), syntax.ref("stmts")), syntax.term("end"), syntax.ref("eol") + }, + "catch-stmt" : { + syntax.term("catch"), syntax.token("id"), syntax.optional(syntax.term(":"), syntax.ref("visit-expr")), syntax.ref("eol") + }, + "function-stmt" : { + syntax.term("function"), syntax.token("id"), syntax.term("("), syntax.ref("nl"), syntax.optional(syntax.ref("argument-list")), syntax.ref("nl"), syntax.term(")"), syntax.optional(syntax.term("override")), syntax.ref("function-body") + }, + "async-function-stmt" : { + syntax.term("async"), syntax.term("function"), syntax.token("id"), + syntax.term("("), syntax.ref("nl"), syntax.optional(syntax.ref("argument-list")), syntax.ref("nl"), syntax.term(")"), + syntax.optional(syntax.term("override")), syntax.ref("function-body") + }, + "function-body" : {syntax.cond_or( + {syntax.term("{"), syntax.ref("stmts"), syntax.term("}")}, + {syntax.ref("eol"), syntax.ref("stmts"), syntax.term("end"), syntax.ref("eol")} + )}, + "class-stmt" : { + syntax.cond_or({syntax.term("class")}, {syntax.term("struct")}), syntax.token("id"), syntax.optional(syntax.term("extends"), syntax.ref("visit-expr")), syntax.ref("eol"), + syntax.ref("decl-stmts"), syntax.term("end"), syntax.ref("eol") + }, + "end-stmt" : { + syntax.term("end"), syntax.ref("eol") + }, + # === Expressions (nl allows multi-line; nlook(endl) prevents ambiguous continuation) === + "expr" : { + syntax.ref("basic-expr"), syntax.optional(syntax.term(","), syntax.ref("nl"), syntax.ref("expr")) + }, + "basic-expr" : {syntax.cond_or( + {syntax.ref("bind-expr"), syntax.term("="), syntax.ref("nl"), syntax.ref("cond-expr")}, + {syntax.ref("cond-expr"), syntax.optional(syntax.ref("asi-op"), syntax.ref("nl"), syntax.ref("basic-expr"))} + )}, + "bind-expr" : { + syntax.term("("), syntax.ref("nl"), syntax.ref("bind-list"), syntax.repeat(syntax.term(","), syntax.ref("nl"), syntax.ref("bind-list")), syntax.ref("nl"), syntax.term(")") + }, + "bind-list" : {syntax.cond_or( + {syntax.token("id")}, + {syntax.term("...")}, + {syntax.ref("bind-expr")} + )}, + "asi-op" : {syntax.cond_or( + {syntax.term("=")}, + {syntax.term(":=")}, + {syntax.term("+=")}, + {syntax.term("-=")}, + {syntax.term("*=")}, + {syntax.term("/=")}, + {syntax.term("%=")}, + {syntax.term("^=")} + )}, + "async-lambda-expr" : { + syntax.term("async"), syntax.term("["), syntax.optional(syntax.ref("capture-list")), syntax.term("]"), + syntax.term("("), syntax.ref("nl"), syntax.optional(syntax.ref("argument-list")), syntax.ref("nl"), syntax.term(")"), + syntax.ref("lambda-body") + }, + "lambda-expr" : { + syntax.term("["), syntax.optional(syntax.ref("capture-list")), syntax.term("]"), + syntax.term("("), syntax.ref("nl"), syntax.optional(syntax.ref("argument-list")), syntax.ref("nl"), syntax.term(")"), + syntax.ref("lambda-body") + }, + "capture-list" : { + syntax.optional(syntax.term("=")), syntax.token("id"), syntax.repeat(syntax.term(","), syntax.ref("nl"), syntax.ref("capture-list")) + }, + "argument-list" : {syntax.cond_or( + {syntax.term("..."), syntax.token("id")}, + {syntax.optional(syntax.term("=")), syntax.token("id"), syntax.optional(syntax.term(":"), syntax.ref("visit-expr")), syntax.repeat(syntax.term(","), syntax.ref("nl"), syntax.ref("argument-list"))} + )}, + "lambda-body" : {syntax.cond_or( + {syntax.term("{"), syntax.ref("stmts"), syntax.term("}")}, + {syntax.term("->"), syntax.ref("cond-expr")} + )}, + "cond-expr" : {syntax.cond_or( + {syntax.ref("async-lambda-expr")}, + {syntax.ref("lambda-expr")}, + {syntax.ref("logic-or-expr"), syntax.optional(syntax.ref("cond-postfix"))} + )}, + "cond-postfix" : {syntax.cond_or( + {syntax.term("?"), syntax.ref("nl"), syntax.ref("value-expr"), syntax.term(":"), syntax.ref("nl"), syntax.ref("cond-expr")}, + {syntax.term(":"), syntax.ref("nl"), syntax.ref("value-expr")} + )}, + "value-expr" : {syntax.cond_or( + {syntax.ref("async-lambda-expr")}, + {syntax.ref("lambda-expr")}, + {syntax.ref("logic-or-expr")} + )}, + "logic-or-expr" : { + syntax.ref("logic-and-expr"), syntax.optional(syntax.cond_or({syntax.term("||")}, {syntax.term("or")}), syntax.ref("nl"), syntax.ref("logic-or-expr")) + }, + "logic-and-expr" : { + syntax.ref("equal-expr"), syntax.optional(syntax.cond_or({syntax.term("&&")}, {syntax.term("and")}), syntax.ref("nl"), syntax.ref("logic-and-expr")) + }, + "equal-expr" : { + syntax.ref("relat-expr"), syntax.optional(syntax.cond_or({syntax.term("==")}, {syntax.term("!=")}, {syntax.term("is")}, {syntax.term("not")}), syntax.ref("nl"), syntax.ref("equal-expr")) + }, + "relat-expr" : { + syntax.ref("add-expr"), syntax.optional(syntax.cond_or({syntax.term(">")}, {syntax.term("<")}, {syntax.term(">=")}, {syntax.term("<=")}), syntax.ref("nl"), syntax.ref("relat-expr")) + }, + "add-expr" : { + syntax.ref("mul-expr"), syntax.optional(syntax.cond_or({syntax.term("+")}, {syntax.term("-")}), syntax.ref("nl"), syntax.ref("add-expr")) + }, + "mul-expr" : { + syntax.ref("conv-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.term("*")}, {syntax.term("/")}, {syntax.term("%")}, {syntax.term("^")}), syntax.ref("nl"), syntax.ref("mul-expr")) + }, + "conv-expr" : { + syntax.ref("unary-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.term("=>")}, {syntax.term("as")}), syntax.ref("nl"), syntax.ref("visit-expr")) + }, + "unary-expr" : {syntax.cond_or( + {syntax.ref("unary-op"), syntax.ref("unary-expr")}, + {syntax.cond_or({syntax.term("new")}, {syntax.term("gcnew")}), syntax.ref("visit-expr"), syntax.optional(syntax.ref("array"))}, + {syntax.ref("prim-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.ref("postfix-expr"))} + )}, + "unary-op" : {syntax.cond_or( + {syntax.term("typeid")}, + {syntax.term("++")}, + {syntax.term("--")}, + {syntax.term("*")}, + {syntax.term("&")}, + {syntax.term("-")}, + {syntax.term("!")}, + {syntax.term("not")} + )}, + "postfix-expr" : { + syntax.cond_or({syntax.term("++")}, {syntax.term("--")}, {syntax.term("...")}), syntax.optional(syntax.ref("postfix-expr")) + }, + "await-expr" : { + syntax.term("await"), syntax.ref("unary-expr") + }, + "prim-expr" : {syntax.cond_or( + {syntax.ref("await-expr")}, + {syntax.ref("visit-expr")}, + {syntax.ref("constant")} + )}, + "visit-expr" : { + syntax.ref("object"), syntax.optional(syntax.cond_or({syntax.term("->")}, {syntax.term(".")}), syntax.ref("nl"), syntax.ref("visit-expr")) + }, + "object" : {syntax.cond_or( + {syntax.ref("array"), syntax.optional(syntax.ref("index"))}, + {syntax.token("str"), syntax.optional(syntax.ref("index"))}, + {syntax.term("local")}, + {syntax.term("global")}, + {syntax.ref("ecsx-extend")}, + {syntax.ref("element")}, + {syntax.token("char")} + )}, + "ecsx-extend" : { + syntax.token("id"), syntax.nlook(syntax.token("endl")), syntax.term("::"), syntax.token("id"), + syntax.term("("), syntax.ref("nl"), syntax.optional(syntax.ref("basic-expr")), syntax.ref("nl"), syntax.term(")") + }, + "element" : { + syntax.cond_or({syntax.token("id")}, {syntax.term("("), syntax.ref("nl"), syntax.ref("basic-expr"), syntax.ref("nl"), syntax.term(")")}), + syntax.repeat(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.ref("fcall")}, {syntax.ref("index")})) + }, + "constant" : {syntax.cond_or( + {syntax.token("num")}, + {syntax.term("null")}, + {syntax.term("true")}, + {syntax.term("false")} + )}, + "array" : { + syntax.term("{"), syntax.ref("nl"), syntax.optional(syntax.ref("expr")), syntax.ref("nl"), syntax.term("}") + }, + "fcall" : { + syntax.term("("), syntax.ref("nl"), syntax.optional(syntax.ref("expr")), syntax.ref("nl"), syntax.term(")") + }, + "index" : {syntax.cond_or( + {syntax.term("["), syntax.ref("nl"), syntax.optional(syntax.ref("add-expr")), syntax.optional(syntax.term(":"), syntax.ref("nl"), syntax.optional(syntax.ref("add-expr")), syntax.optional(syntax.term(":"), syntax.ref("nl"), syntax.optional(syntax.ref("add-expr")))), syntax.ref("nl"), syntax.term("]")}, + {syntax.term("["), syntax.term("::"), syntax.term("]")} + )} + }.to_hash_map() + @end +end + +function get_fmt_grammar() + var g = new parsergen.grammar + g.ext = ".*\\.(csp|csc|ecs|ecsx)" + g.lex = get_fmt_lexical(regex.build_optimize) + g.stx := get_fmt_syntax() + return g +end + +# Verify format grammar covers all main grammar rules (call from tests) +function check_fmt_syntax_sync() + var main_stx = covscript_syntax + var fmt_stx = get_fmt_syntax() + var fmt_extra = new hash_map + fmt_extra["nl"] = 1 + fmt_extra["eos"] = 1 + fmt_extra["eol"] = 1 + var main_extra = new hash_map + main_extra["ignore"] = 1 + var missing = new array + foreach it in main_stx + var name = it.first + if !main_extra.exist(name) && !fmt_stx.exist(name) + missing.push_back(name) + end + end + foreach it in fmt_stx + var name = it.first + if !fmt_extra.exist(name) && !main_stx.exist(name) + missing.push_back("fmt-only:" + name) + end + end + return missing +end + var grammar = new parsergen.grammar grammar.ext = ".*\\.(csp|csc|ecs|ecsx)" grammar.lex = get_lexical(regex.build_optimize) diff --git a/unit_tests/test_lint.ecs b/unit_tests/test_lint.ecs new file mode 100644 index 0000000..b9541a8 --- /dev/null +++ b/unit_tests/test_lint.ecs @@ -0,0 +1,38 @@ +# Linter test file - intentionally contains style issues for lint verification +# === Naming convention violations === +function CamelCase() + return 1 +end +var BadName = 2 +constant bad_constant = 3 +class BadClass + var x = 0 +end +# === Unreachable code === +function unreachable_test() + return 1 + var dead_code = 2 +end +# === Empty block === +if true + # empty +end +var trailing = 1 +# === Consecutive blank lines (intentional) === +var a = 1 +var b = 2 +# === Useless expression === +var c = 1 +c +# === Properly formatted code (should pass clean) === +function good_function(x, y) + var result = x + y + foreach item in result + if item > 0 + return item + else + continue + end + end + return result +end From f80506b5b56b230b2339a735f6d8106037e509e7 Mon Sep 17 00:00:00 2001 From: Mike Lee Date: Wed, 22 Jul 2026 16:30:23 +0800 Subject: [PATCH 02/13] Refactor ECS parser and formatter to support strict and non-strict modes - Introduced `get_fmt_grammar` and `get_fmt_syntax` functions to handle format-specific grammar and syntax. - Updated `get_lexical` to accept a `strict` parameter, allowing for different lexical rules based on mode. - Modified grammar rules to differentiate between strict and non-strict parsing, particularly in handling comments and newlines. - Enhanced syntax definitions to use function pointers for end-of-line and end-of-statement handling based on the parsing mode. - Removed redundant code and improved overall structure for better maintainability and readability. --- imports/ecs_bootstrap.csp | 14 +- imports/ecs_format.csp | 10 +- imports/ecs_parser.csp | 611 ++++++++------------------------------ 3 files changed, 141 insertions(+), 494 deletions(-) diff --git a/imports/ecs_bootstrap.csp b/imports/ecs_bootstrap.csp index 3ead3da..ac2e588 100644 --- a/imports/ecs_bootstrap.csp +++ b/imports/ecs_bootstrap.csp @@ -370,7 +370,7 @@ function setup_unicode(charset) return null end var cvt = codecvt_map.at(cvt_name)(unicode_ext) - ecs_parser.grammar.lex = ecs_parser.get_lexical([](str)->unicode_ext.build_optimize_wregex(cvt.local2wide(str))) + ecs_parser.grammar.lex = ecs_parser.get_lexical([](str)->unicode_ext.build_optimize_wregex(cvt.local2wide(str)), false) if cvt_name == "GBK" var wrapper = new gbk_wrapper wrapper.codecvt = cvt @@ -452,15 +452,11 @@ function run_ecs(cmd_args) return exit_code end end - # Lint-only mode (uses main grammar, no codegen) + # Lint-only mode (lint_file does its own parsing) if lint_only - parser.add_grammar("ecs-lang", ecs_parser.grammar) - parser.from_file(file_name) - if parser.ast != null - var result = ecs_lint.lint_file(file_name) - if result != null - result[0].print_report() - end + var result = ecs_lint.lint_file(file_name) + if result != null + result[0].print_report() end return 0 end diff --git a/imports/ecs_format.csp b/imports/ecs_format.csp index 5923753..0f652f0 100644 --- a/imports/ecs_format.csp +++ b/imports/ecs_format.csp @@ -22,6 +22,14 @@ package ecs_format import parsergen, ecs_parser, ecs_lint, regex +function get_fmt_grammar() + var g = new parsergen.grammar + g.ext = ".*\\.(csp|csc|ecs|ecsx)" + g.lex = ecs_parser.get_lexical(regex.build_optimize, true) + g.stx := ecs_parser.get_syntax(true) + return g +end + # ============================================================ # Format Writer # ============================================================ @@ -984,7 +992,7 @@ function format_file(file_name, unicode_cvt) if unicode_cvt != null parser.unicode_cvt = unicode_cvt end - parser.add_grammar("ecs-lang-fmt", ecs_parser.get_fmt_grammar()) + parser.add_grammar("ecs-lang-fmt", get_fmt_grammar()) parser.from_file(file_name) if parser.ast == null system.out.println("Error: Failed to parse file '" + file_name + "'.") diff --git a/imports/ecs_parser.csp b/imports/ecs_parser.csp index 058bc0d..84237dc 100644 --- a/imports/ecs_parser.csp +++ b/imports/ecs_parser.csp @@ -24,9 +24,9 @@ import parsergen, regex constant syntax = parsergen.syntax -function get_lexical(reg_builder) +function get_lexical(reg_builder, strict) @begin - return { + var lex = { "endl" : reg_builder("^\\n+$"), "id" : reg_builder("^[A-Za-z_\\p{Han}](\\w|\\p{Han})*$"), "num" : reg_builder("^[0-9]+\\.?([0-9]+)?$"), @@ -37,382 +37,54 @@ function get_lexical(reg_builder) "lsig" : reg_builder("^(>|<|&|(\\|)|&&|(\\|\\|)|!|=(=|>)?|!=?|>=?|<=?)$"), "brac" : reg_builder("^(\\(|\\)|\\[|\\]|\\{|\\}|,)$"), "prep" : reg_builder("^@.*$"), - "ign" : reg_builder("^([ \\f\\r\\t]+|#.*)$"), "err" : reg_builder("^(\"|\'|(\\|)|\\.\\.)$") }.to_hash_map() @end -end - -function get_fmt_lexical(reg_builder) - var lex = get_lexical(reg_builder) - lex["ign"] = reg_builder("^[ \\f\\r\\t]+$") - lex["com"] = reg_builder("^#.*$") + if strict + lex["com"] = reg_builder("^#.*$") + lex["ign"] = reg_builder("^[ \\f\\r\\t]+$") + else + lex["ign"] = reg_builder("^([ \\f\\r\\t]+|#.*)$") + end return lex end -@begin -var covscript_syntax = { - # Beginning of Parsing - "begin" : { - syntax.ref("stmts") - }, - # Ignore if not match initiatively - "ignore" : { - syntax.repeat(syntax.token("endl")) - }, - # End of Line - "endline" : {syntax.cond_or( - {syntax.token("endl")}, - {syntax.term(";")} - )}, - # Bootstrap - "stmts" : { - syntax.repeat(syntax.nlook(syntax.ref("endblock")), syntax.ref("statement"), syntax.repeat(syntax.token("endl"))) - }, - "decl-stmts" : { - syntax.repeat(syntax.nlook(syntax.ref("endblock")), syntax.ref("declaration"), syntax.repeat(syntax.token("endl"))) - }, - "endblock" : {syntax.cond_or( - {syntax.ref("end-stmt")}, - {syntax.ref("else-stmt")}, - {syntax.ref("until-stmt")}, - {syntax.ref("catch-stmt")} - )}, - "statement" : {syntax.cond_or( - {syntax.ref("prep-stmt")}, - {syntax.ref("package-stmt")}, - {syntax.ref("import-stmt")}, - {syntax.ref("var-stmt")}, - {syntax.ref("block-stmt")}, - {syntax.ref("namespace-stmt")}, - {syntax.ref("using-stmt")}, - {syntax.ref("if-stmt")}, - {syntax.ref("switch-stmt")}, - {syntax.ref("while-stmt")}, - {syntax.ref("loop-stmt")}, - {syntax.ref("for-stmt")}, - {syntax.ref("foreach-stmt")}, - {syntax.ref("async-function-stmt")}, - {syntax.ref("control-stmt")}, - {syntax.ref("function-stmt")}, - {syntax.ref("yield-stmt")}, - {syntax.ref("return-stmt")}, - {syntax.ref("try-stmt")}, - {syntax.ref("throw-stmt")}, - {syntax.ref("class-stmt")}, - {syntax.ref("expr-stmt")} - )}, - "declaration" : {syntax.cond_or( - {syntax.ref("prep-stmt")}, - {syntax.ref("namespace-stmt")}, - {syntax.ref("var-stmt")}, - {syntax.ref("using-stmt")}, - {syntax.ref("async-function-stmt")}, - {syntax.ref("function-stmt")}, - {syntax.ref("class-stmt")} - )}, - # Statements - "prep-stmt" : { - syntax.token("prep"), syntax.token("endl") - }, - "package-stmt" : { - syntax.term("package"), syntax.token("id"), syntax.ref("endline") - }, - "import-stmt" : { - syntax.term("import"), syntax.ref("import-list"), syntax.ref("endline") - }, - "module-list" : { - syntax.token("id"), syntax.optional(syntax.term("."), syntax.cond_or({syntax.term("*")}, {syntax.ref("module-list")})) - }, - "import-list" : { - syntax.ref("module-list"), syntax.optional(syntax.term("as"), syntax.token("id")), syntax.optional(syntax.term(","), syntax.ref("import-list")) - }, - "var-def" : { - syntax.cond_or({syntax.ref("var-bind"), syntax.term("="), syntax.ref("basic-expr")}, {syntax.ref("var-list")}) - }, - "var-stmt" : { - syntax.cond_or({syntax.term("var")}, {syntax.term("link")}, {syntax.term("constant")}), syntax.ref("var-def"), syntax.ref("endline") - }, - "var-bind" : { - syntax.term("("), syntax.ref("var-bind-list"), syntax.repeat(syntax.term(","), syntax.ref("var-bind-list")), syntax.term(")") - }, - "var-bind-list" : {syntax.cond_or( - {syntax.token("id")}, - {syntax.term("...")}, - {syntax.ref("var-bind")} - )}, - "var-list" : { - syntax.token("id"), syntax.cond_or( - {syntax.term("="), syntax.ref("basic-expr")}, - {syntax.term("as"), syntax.ref("unary-expr"), syntax.optional(syntax.ref("array"))} - ), syntax.optional(syntax.term(","), syntax.ref("var-list")) - }, - "block-stmt" : { - syntax.term("block"), syntax.token("endl"), syntax.ref("stmts"), syntax.term("end"), syntax.token("endl") - }, - "namespace-stmt" : { - syntax.term("namespace"), syntax.token("id"), syntax.token("endl"), syntax.ref("decl-stmts"), syntax.term("end"), syntax.token("endl") - }, - "using-stmt" : { - syntax.term("using"), syntax.ref("using-list"), syntax.ref("endline") - }, - "using-list" : { - syntax.ref("module-list"), syntax.optional(syntax.term(","), syntax.ref("using-list")) - }, - "if-stmt" : { - syntax.term("if"), syntax.ref("basic-expr"), syntax.token("endl"), syntax.ref("stmts"), syntax.repeat(syntax.ref("else-stmt"), syntax.ref("stmts")), syntax.term("end"), syntax.token("endl") - }, - "else-stmt" : { - syntax.term("else"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.term("if"), syntax.ref("basic-expr")), syntax.token("endl") - }, - "switch-stmt" : { - syntax.term("switch"), syntax.ref("basic-expr"), syntax.token("endl"), syntax.ref("switch-stmts"), syntax.term("end"), syntax.token("endl") - }, - "switch-stmts" : { - syntax.repeat(syntax.cond_or({syntax.ref("switch-case")}, {syntax.ref("switch-default")}), syntax.repeat(syntax.token("endl"))) - }, - "switch-case" : { - syntax.term("case"), syntax.ref("logic-or-expr"), syntax.token("endl"), syntax.ref("stmts"), syntax.term("end"), syntax.token("endl") - }, - "switch-default" : { - syntax.term("default"), syntax.token("endl"), syntax.ref("stmts"), syntax.term("end"), syntax.token("endl") - }, - "while-stmt" : { - syntax.term("while"), syntax.ref("basic-expr"), syntax.token("endl"), syntax.ref("stmts"), syntax.term("end"), syntax.token("endl") - }, - "loop-stmt" : { - syntax.term("loop"), syntax.token("endl"), syntax.ref("stmts"), syntax.cond_or({syntax.ref("until-stmt")}, {syntax.term("end"), syntax.token("endl")}) - }, - "until-stmt" : { - syntax.term("until"), syntax.ref("basic-expr"), syntax.token("endl") - }, - "for-stmt" : { - syntax.term("for"), syntax.optional(syntax.ref("var-def")), syntax.cond_or({syntax.term(";")}, {syntax.term(",")}), syntax.optional(syntax.ref("basic-expr")), syntax.cond_or({syntax.term(";")}, {syntax.term(",")}), syntax.optional(syntax.ref("basic-expr")), syntax.ref("for-body") - }, - "foreach-stmt" : { - syntax.term("foreach"), syntax.optional(syntax.nlook(syntax.term("in")), syntax.token("id")), syntax.term("in"), syntax.ref("basic-expr"), syntax.ref("for-body") - }, - "for-body" : {syntax.cond_or( - {syntax.term("do"), syntax.ref("basic-expr"), syntax.ref("endline")}, - {syntax.token("endl"), syntax.ref("stmts"), syntax.term("end"), syntax.token("endl")} - )}, - "function-stmt" : { - syntax.term("function"), syntax.token("id"), syntax.term("("), syntax.optional(syntax.ref("argument-list")), syntax.term(")"), syntax.optional(syntax.term("override")), syntax.ref("function-body") - }, - "function-body" : {syntax.cond_or( - {syntax.term("{"), syntax.ref("stmts"), syntax.term("}")}, - {syntax.token("endl"), syntax.ref("stmts"), syntax.term("end"), syntax.token("endl")} - )}, - "return-stmt" : { - syntax.term("return"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.ref("expr")), syntax.ref("endline") - }, - "try-stmt" : { - syntax.term("try"), syntax.token("endl"), syntax.ref("stmts"), syntax.repeat(syntax.ref("catch-stmt"), syntax.ref("stmts")), syntax.term("end"), syntax.token("endl") - }, - "catch-stmt" : { - syntax.term("catch"), syntax.token("id"), syntax.optional(syntax.term(":"), syntax.ref("visit-expr")), syntax.token("endl") - }, - "throw-stmt" : { - syntax.term("throw"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.ref("expr")), syntax.ref("endline") - }, - "class-stmt" : { - syntax.cond_or({syntax.term("class")}, {syntax.term("struct")}), syntax.token("id"), syntax.optional(syntax.term("extends"), syntax.ref("visit-expr")), syntax.token("endl"), - syntax.ref("decl-stmts"), syntax.term("end"), syntax.token("endl") - }, - "async-function-stmt" : { - syntax.term("async"), syntax.term("function"), syntax.token("id"), - syntax.term("("), syntax.optional(syntax.ref("argument-list")), syntax.term(")"), - syntax.optional(syntax.term("override")), syntax.ref("function-body") - }, - "yield-stmt" : { - syntax.term("yield"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.ref("expr")), syntax.ref("endline") - }, - "control-stmt" : { - syntax.cond_or({syntax.term("break")}, {syntax.term("continue")}), syntax.ref("endline") - }, - "expr-stmt" : { - syntax.ref("expr"), syntax.ref("endline") - }, - "end-stmt" : { - syntax.term("end"), syntax.token("endl") - }, - # Expression - "expr" : { - syntax.ref("basic-expr"), syntax.optional(syntax.term(","), syntax.ref("expr")) - }, - "bind-expr" : { - syntax.term("("), syntax.ref("bind-list"), syntax.repeat(syntax.term(","), syntax.ref("bind-list")), syntax.term(")") - }, - "bind-list" : {syntax.cond_or( - {syntax.token("id")}, - {syntax.term("...")}, - {syntax.ref("bind-expr")} - )}, - "basic-expr" : {syntax.cond_or( - {syntax.ref("bind-expr"), syntax.term("="), syntax.ref("cond-expr")}, - {syntax.ref("cond-expr"), syntax.optional(syntax.ref("asi-op"), syntax.ref("basic-expr"))} - )}, - "asi-op" : {syntax.cond_or( - {syntax.term("=")}, - {syntax.term(":=")}, - {syntax.term("+=")}, - {syntax.term("-=")}, - {syntax.term("*=")}, - {syntax.term("/=")}, - {syntax.term("%=")}, - {syntax.term("^=")} - )}, - "async-lambda-expr" : { - syntax.term("async"), syntax.term("["), syntax.optional(syntax.ref("capture-list")), syntax.term("]"), - syntax.term("("), syntax.optional(syntax.ref("argument-list")), syntax.term(")"), - syntax.ref("lambda-body") - }, - "lambda-expr" : { - syntax.term("["), syntax.optional(syntax.ref("capture-list")), syntax.term("]"), syntax.term("("), syntax.optional(syntax.ref("argument-list")), syntax.term(")"), syntax.ref("lambda-body") - }, - "capture-list" : { - syntax.optional(syntax.term("=")), syntax.token("id"), syntax.repeat(syntax.term(","), syntax.ref("capture-list")) - }, - "argument-list" : {syntax.cond_or( - {syntax.term("..."), syntax.token("id")}, - {syntax.optional(syntax.term("=")), syntax.token("id"), syntax.optional(syntax.term(":"), syntax.ref("visit-expr")), syntax.repeat(syntax.term(","), syntax.ref("argument-list"))} - )}, - "lambda-body" : {syntax.cond_or( - {syntax.term("{"), syntax.repeat(syntax.ref("statement"), syntax.repeat(syntax.token("endl"))), syntax.term("}")}, - {syntax.term("->"), syntax.ref("cond-expr")} - )}, - "cond-expr" : {syntax.cond_or( - {syntax.ref("async-lambda-expr")}, - {syntax.ref("lambda-expr")}, - {syntax.ref("logic-or-expr"), syntax.optional(syntax.ref("cond-postfix"))} - )}, - "cond-postfix" : {syntax.cond_or( - {syntax.term("?"), syntax.ref("value-expr"), syntax.term(":"), syntax.ref("cond-expr")}, - {syntax.term(":"), syntax.ref("value-expr")} - )}, - "value-expr" : {syntax.cond_or( - {syntax.ref("async-lambda-expr")}, - {syntax.ref("lambda-expr")}, - {syntax.ref("logic-or-expr")} - )}, - "logic-or-expr" : { - syntax.ref("logic-and-expr"), syntax.optional(syntax.cond_or({syntax.term("||")}, {syntax.term("or")}), syntax.ref("logic-or-expr")) - }, - "logic-and-expr" : { - syntax.ref("equal-expr"), syntax.optional(syntax.cond_or({syntax.term("&&")}, {syntax.term("and")}), syntax.ref("logic-and-expr")) - }, - "equal-expr" : { - syntax.ref("relat-expr"), syntax.optional(syntax.cond_or({syntax.term("==")}, {syntax.term("!=")}, {syntax.term("is")}, {syntax.term("not")}), syntax.ref("equal-expr")) - }, - "relat-expr" : { - syntax.ref("add-expr"), syntax.optional(syntax.cond_or({syntax.term(">")}, {syntax.term("<")}, {syntax.term(">=")}, {syntax.term("<=")}), syntax.ref("relat-expr")) - }, - "add-expr" : { - syntax.ref("mul-expr"), syntax.optional(syntax.cond_or({syntax.term("+")}, {syntax.term("-")}), syntax.ref("add-expr")) - }, - "mul-expr" : { - syntax.ref("conv-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.term("*")}, {syntax.term("/")}, {syntax.term("%")}, {syntax.term("^")}), syntax.ref("mul-expr")) - }, - "conv-expr" : { - syntax.ref("unary-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.term("=>")}, {syntax.term("as")}), syntax.ref("visit-expr")) - }, - "unary-expr" : {syntax.cond_or( - {syntax.ref("unary-op"), syntax.ref("unary-expr")}, - {syntax.cond_or({syntax.term("new")}, {syntax.term("gcnew")}), syntax.ref("visit-expr"), syntax.optional(syntax.ref("array"))}, - {syntax.ref("prim-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.ref("postfix-expr"))} - )}, - "unary-op" : {syntax.cond_or( - {syntax.term("typeid")}, - {syntax.term("++")}, - {syntax.term("--")}, - {syntax.term("*")}, - {syntax.term("&")}, - {syntax.term("-")}, - {syntax.term("!")}, - {syntax.term("not")} - )}, - "postfix-expr" : { - syntax.cond_or({syntax.term("++")}, {syntax.term("--")}, {syntax.term("...")}), syntax.optional(syntax.ref("postfix-expr")) - }, - "await-expr" : { - syntax.term("await"), syntax.ref("unary-expr") - }, - "prim-expr" : {syntax.cond_or( - {syntax.ref("await-expr")}, - {syntax.ref("visit-expr")}, - {syntax.ref("constant")} - )}, - "visit-expr" : { - syntax.ref("object"), syntax.optional(syntax.cond_or({syntax.term("->")}, {syntax.term(".")}), syntax.ref("visit-expr")) - }, - "object" : {syntax.cond_or( - {syntax.ref("array"), syntax.optional(syntax.ref("index"))}, - {syntax.token("str"), syntax.optional(syntax.ref("index"))}, - {syntax.term("local")}, - {syntax.term("global")}, - {syntax.ref("ecsx-extend")}, - {syntax.ref("element")}, - {syntax.token("char")} - )}, - "ecsx-extend" : { - syntax.token("id"), syntax.nlook(syntax.token("endl")), syntax.term("::"), syntax.token("id"), syntax.term("("), syntax.optional(syntax.ref("basic-expr")), syntax.term(")") - }, - "element" : { - syntax.cond_or({syntax.token("id")}, {syntax.term("("), syntax.ref("basic-expr"), syntax.term(")")}), - syntax.repeat(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.ref("fcall")}, {syntax.ref("index")})) - }, - "constant" : {syntax.cond_or( - {syntax.token("num")}, - {syntax.term("null")}, - {syntax.term("true")}, - {syntax.term("false")} - )}, - "array" : { - syntax.term("{"), syntax.optional(syntax.ref("expr")), syntax.term("}") - }, - "fcall" : { - syntax.term("("), syntax.optional(syntax.ref("expr")), syntax.term(")") - }, - "index" : {syntax.cond_or( - {syntax.term("["), syntax.optional(syntax.ref("add-expr")), syntax.optional(syntax.term(":"), syntax.optional(syntax.ref("add-expr")), syntax.optional(syntax.term(":"), syntax.optional(syntax.ref("add-expr")))), syntax.term("]")}, - {syntax.term("["), syntax.term("::"), syntax.term("]")} - )} -}.to_hash_map() -@end - -# ============================================================ -# Format-specific syntax — comments are explicit "com" tokens -# ============================================================ - -function get_fmt_syntax() +function get_syntax(strict) + var syntax_eol_fn = null + var syntax_eos_fn = null + var syntax_nl_fn = null + if strict + syntax_eol_fn = []()->syntax.ref("eol") + syntax_eos_fn = []()->syntax.ref("eos") + syntax_nl_fn = []()->{syntax.ref("nl")} + else + syntax_eol_fn = []()->syntax.token("endl") + syntax_eos_fn = []()->syntax.token("endl") + syntax_nl_fn = []()->{} + end @begin - return { - # === Helper rules (NO ignore rule — all newlines are explicit) === - # nl: zero or more newlines (for multi-line expressions) + var stx = { + # Helper: newlines within multi-line expressions + # non-strict: ignore eats endl -> nl matches zero (no-op) + # strict: no ignore -> nl consumes visible endl tokens "nl" : {syntax.repeat(syntax.token("endl"))}, - # eol: end-of-statement-line = optional inline comment + newline - "eol" : {syntax.optional(syntax.token("com")), syntax.token("endl")}, - # eos: statement separator = standalone comment line OR blank line - "eos" : {syntax.cond_or( - {syntax.token("com"), syntax.token("endl")}, - {syntax.token("endl")} - )}, - # endline: eol or semicolon - "endline" : {syntax.cond_or( - {syntax.ref("eol")}, - {syntax.term(";")} - )}, - # === Bootstrap === + # Beginning of Parsing "begin" : { syntax.ref("stmts") }, + # End of Line + "endline" : {syntax.cond_or( + {syntax_eol_fn()}, + {syntax.term(";")} + )}, + # Bootstrap "stmts" : { - syntax.repeat(syntax.ref("eos")), - syntax.repeat(syntax.nlook(syntax.ref("endblock")), syntax.ref("statement"), syntax.repeat(syntax.ref("eos"))) + syntax.repeat(syntax_eos_fn()), + syntax.repeat(syntax.nlook(syntax.ref("endblock")), syntax.ref("statement"), syntax.repeat(syntax_eos_fn())) }, "decl-stmts" : { - syntax.repeat(syntax.ref("eos")), - syntax.repeat(syntax.nlook(syntax.ref("endblock")), syntax.ref("declaration"), syntax.repeat(syntax.ref("eos"))) + syntax.repeat(syntax_eos_fn()), + syntax.repeat(syntax.nlook(syntax.ref("endblock")), syntax.ref("declaration"), syntax.repeat(syntax_eos_fn())) }, "endblock" : {syntax.cond_or( {syntax.ref("end-stmt")}, @@ -453,9 +125,9 @@ function get_fmt_syntax() {syntax.ref("function-stmt")}, {syntax.ref("class-stmt")} )}, - # === Simple statements === + # Statements "prep-stmt" : { - syntax.token("prep"), syntax.ref("eol") + syntax.token("prep"), syntax_eol_fn() }, "package-stmt" : { syntax.term("package"), syntax.token("id"), syntax.ref("endline") @@ -470,13 +142,13 @@ function get_fmt_syntax() syntax.ref("module-list"), syntax.optional(syntax.term("as"), syntax.token("id")), syntax.optional(syntax.term(","), syntax.ref("import-list")) }, "var-def" : { - syntax.cond_or({syntax.ref("var-bind"), syntax.term("="), syntax.ref("nl"), syntax.ref("basic-expr")}, {syntax.ref("var-list")}) + syntax.cond_or({syntax.ref("var-bind"), syntax.term("="), (syntax_nl_fn())..., syntax.ref("basic-expr")}, {syntax.ref("var-list")}) }, "var-stmt" : { syntax.cond_or({syntax.term("var")}, {syntax.term("link")}, {syntax.term("constant")}), syntax.ref("var-def"), syntax.ref("endline") }, "var-bind" : { - syntax.term("("), syntax.ref("nl"), syntax.ref("var-bind-list"), syntax.repeat(syntax.term(","), syntax.ref("nl"), syntax.ref("var-bind-list")), syntax.ref("nl"), syntax.term(")") + syntax.term("("), (syntax_nl_fn())..., syntax.ref("var-bind-list"), syntax.repeat(syntax.term(","), (syntax_nl_fn())..., syntax.ref("var-bind-list")), (syntax_nl_fn())..., syntax.term(")") }, "var-bind-list" : {syntax.cond_or( {syntax.token("id")}, @@ -485,9 +157,15 @@ function get_fmt_syntax() )}, "var-list" : { syntax.token("id"), syntax.cond_or( - {syntax.term("="), syntax.ref("nl"), syntax.ref("basic-expr")}, + {syntax.term("="), (syntax_nl_fn())..., syntax.ref("basic-expr")}, {syntax.term("as"), syntax.ref("unary-expr"), syntax.optional(syntax.ref("array"))} - ), syntax.optional(syntax.term(","), syntax.ref("nl"), syntax.ref("var-list")) + ), syntax.optional(syntax.term(","), (syntax_nl_fn())..., syntax.ref("var-list")) + }, + "block-stmt" : { + syntax.term("block"), syntax_eol_fn(), syntax.ref("stmts"), syntax.term("end"), syntax_eol_fn() + }, + "namespace-stmt" : { + syntax.term("namespace"), syntax.token("id"), syntax_eol_fn(), syntax.ref("decl-stmts"), syntax.term("end"), syntax_eol_fn() }, "using-stmt" : { syntax.term("using"), syntax.ref("using-list"), syntax.ref("endline") @@ -495,106 +173,94 @@ function get_fmt_syntax() "using-list" : { syntax.ref("module-list"), syntax.optional(syntax.term(","), syntax.ref("using-list")) }, - "control-stmt" : { - syntax.cond_or({syntax.term("break")}, {syntax.term("continue")}), syntax.ref("endline") - }, - "expr-stmt" : { - syntax.ref("expr"), syntax.ref("endline") - }, - "return-stmt" : { - syntax.term("return"), syntax.optional(syntax.nlook(syntax.ref("eol")), syntax.ref("expr")), syntax.ref("endline") - }, - "throw-stmt" : { - syntax.term("throw"), syntax.optional(syntax.nlook(syntax.ref("eol")), syntax.ref("expr")), syntax.ref("endline") - }, - "yield-stmt" : { - syntax.term("yield"), syntax.optional(syntax.nlook(syntax.ref("eol")), syntax.ref("expr")), syntax.ref("endline") - }, - # === Block statements === - "block-stmt" : { - syntax.term("block"), syntax.ref("eol"), syntax.ref("stmts"), syntax.term("end"), syntax.ref("eol") - }, - "namespace-stmt" : { - syntax.term("namespace"), syntax.token("id"), syntax.ref("eol"), syntax.ref("decl-stmts"), syntax.term("end"), syntax.ref("eol") - }, "if-stmt" : { - syntax.term("if"), syntax.ref("basic-expr"), syntax.ref("eol"), syntax.ref("stmts"), syntax.repeat(syntax.ref("else-stmt"), syntax.ref("stmts")), syntax.term("end"), syntax.ref("eol") + syntax.term("if"), syntax.ref("basic-expr"), syntax_eol_fn(), syntax.ref("stmts"), syntax.repeat(syntax.ref("else-stmt"), syntax.ref("stmts")), syntax.term("end"), syntax_eol_fn() }, "else-stmt" : { - syntax.term("else"), syntax.optional(syntax.nlook(syntax.ref("eol")), syntax.term("if"), syntax.ref("basic-expr")), syntax.ref("eol") + syntax.term("else"), syntax.optional(syntax.nlook(syntax_eol_fn()), syntax.term("if"), syntax.ref("basic-expr")), syntax_eol_fn() }, "switch-stmt" : { - syntax.term("switch"), syntax.ref("basic-expr"), syntax.ref("eol"), syntax.ref("switch-stmts"), syntax.term("end"), syntax.ref("eol") + syntax.term("switch"), syntax.ref("basic-expr"), syntax_eol_fn(), syntax.ref("switch-stmts"), syntax.term("end"), syntax_eol_fn() }, "switch-stmts" : { - syntax.repeat(syntax.ref("eos")), - syntax.repeat(syntax.cond_or({syntax.ref("switch-case")}, {syntax.ref("switch-default")}), syntax.repeat(syntax.ref("eos"))) + syntax.repeat(syntax_eos_fn()), + syntax.repeat(syntax.cond_or({syntax.ref("switch-case")}, {syntax.ref("switch-default")}), syntax.repeat(syntax_eos_fn())) }, "switch-case" : { - syntax.term("case"), syntax.ref("logic-or-expr"), syntax.ref("eol"), syntax.ref("stmts"), syntax.term("end"), syntax.ref("eol") + syntax.term("case"), syntax.ref("logic-or-expr"), syntax_eol_fn(), syntax.ref("stmts"), syntax.term("end"), syntax_eol_fn() }, "switch-default" : { - syntax.term("default"), syntax.ref("eol"), syntax.ref("stmts"), syntax.term("end"), syntax.ref("eol") + syntax.term("default"), syntax_eol_fn(), syntax.ref("stmts"), syntax.term("end"), syntax_eol_fn() }, "while-stmt" : { - syntax.term("while"), syntax.ref("basic-expr"), syntax.ref("eol"), syntax.ref("stmts"), syntax.term("end"), syntax.ref("eol") + syntax.term("while"), syntax.ref("basic-expr"), syntax_eol_fn(), syntax.ref("stmts"), syntax.term("end"), syntax_eol_fn() }, "loop-stmt" : { - syntax.term("loop"), syntax.ref("eol"), syntax.ref("stmts"), syntax.cond_or({syntax.ref("until-stmt")}, {syntax.term("end"), syntax.ref("eol")}) + syntax.term("loop"), syntax_eol_fn(), syntax.ref("stmts"), syntax.cond_or({syntax.ref("until-stmt")}, {syntax.term("end"), syntax_eol_fn()}) }, "until-stmt" : { - syntax.term("until"), syntax.ref("basic-expr"), syntax.ref("eol") + syntax.term("until"), syntax.ref("basic-expr"), syntax_eol_fn() }, "for-stmt" : { - syntax.term("for"), syntax.optional(syntax.ref("var-def")), - syntax.cond_or({syntax.term(";")}, {syntax.term(",")}), syntax.ref("nl"), - syntax.optional(syntax.ref("basic-expr")), - syntax.cond_or({syntax.term(";")}, {syntax.term(",")}), syntax.ref("nl"), - syntax.optional(syntax.ref("basic-expr")), - syntax.ref("for-body") + syntax.term("for"), syntax.optional(syntax.ref("var-def")), syntax.cond_or({syntax.term(";")}, {syntax.term(",")}), (syntax_nl_fn())..., syntax.optional(syntax.ref("basic-expr")), syntax.cond_or({syntax.term(";")}, {syntax.term(",")}), (syntax_nl_fn())..., syntax.optional(syntax.ref("basic-expr")), syntax.ref("for-body") }, "foreach-stmt" : { syntax.term("foreach"), syntax.optional(syntax.nlook(syntax.term("in")), syntax.token("id")), syntax.term("in"), syntax.ref("basic-expr"), syntax.ref("for-body") }, "for-body" : {syntax.cond_or( {syntax.term("do"), syntax.ref("basic-expr"), syntax.ref("endline")}, - {syntax.ref("eol"), syntax.ref("stmts"), syntax.term("end"), syntax.ref("eol")} + {syntax_eol_fn(), syntax.ref("stmts"), syntax.term("end"), syntax_eol_fn()} )}, + "function-stmt" : { + syntax.term("function"), syntax.token("id"), syntax.term("("), (syntax_nl_fn())..., syntax.optional(syntax.ref("argument-list")), (syntax_nl_fn())..., syntax.term(")"), syntax.optional(syntax.term("override")), syntax.ref("function-body") + }, + "function-body" : {syntax.cond_or( + {syntax.term("{"), syntax.ref("stmts"), syntax.term("}")}, + {syntax_eol_fn(), syntax.ref("stmts"), syntax.term("end"), syntax_eol_fn()} + )}, + "return-stmt" : { + syntax.term("return"), syntax.optional(syntax.nlook(syntax_eol_fn()), syntax.ref("expr")), syntax.ref("endline") + }, "try-stmt" : { - syntax.term("try"), syntax.ref("eol"), syntax.ref("stmts"), syntax.repeat(syntax.ref("catch-stmt"), syntax.ref("stmts")), syntax.term("end"), syntax.ref("eol") + syntax.term("try"), syntax_eol_fn(), syntax.ref("stmts"), syntax.repeat(syntax.ref("catch-stmt"), syntax.ref("stmts")), syntax.term("end"), syntax_eol_fn() }, "catch-stmt" : { - syntax.term("catch"), syntax.token("id"), syntax.optional(syntax.term(":"), syntax.ref("visit-expr")), syntax.ref("eol") + syntax.term("catch"), syntax.token("id"), syntax.optional(syntax.term(":"), syntax.ref("visit-expr")), syntax_eol_fn() }, - "function-stmt" : { - syntax.term("function"), syntax.token("id"), syntax.term("("), syntax.ref("nl"), syntax.optional(syntax.ref("argument-list")), syntax.ref("nl"), syntax.term(")"), syntax.optional(syntax.term("override")), syntax.ref("function-body") + "throw-stmt" : { + syntax.term("throw"), syntax.optional(syntax.nlook(syntax_eol_fn()), syntax.ref("expr")), syntax.ref("endline") + }, + "class-stmt" : { + syntax.cond_or({syntax.term("class")}, {syntax.term("struct")}), syntax.token("id"), syntax.optional(syntax.term("extends"), syntax.ref("visit-expr")), syntax_eol_fn(), + syntax.ref("decl-stmts"), syntax.term("end"), syntax_eol_fn() }, "async-function-stmt" : { syntax.term("async"), syntax.term("function"), syntax.token("id"), - syntax.term("("), syntax.ref("nl"), syntax.optional(syntax.ref("argument-list")), syntax.ref("nl"), syntax.term(")"), + syntax.term("("), (syntax_nl_fn())..., syntax.optional(syntax.ref("argument-list")), (syntax_nl_fn())..., syntax.term(")"), syntax.optional(syntax.term("override")), syntax.ref("function-body") }, - "function-body" : {syntax.cond_or( - {syntax.term("{"), syntax.ref("stmts"), syntax.term("}")}, - {syntax.ref("eol"), syntax.ref("stmts"), syntax.term("end"), syntax.ref("eol")} - )}, - "class-stmt" : { - syntax.cond_or({syntax.term("class")}, {syntax.term("struct")}), syntax.token("id"), syntax.optional(syntax.term("extends"), syntax.ref("visit-expr")), syntax.ref("eol"), - syntax.ref("decl-stmts"), syntax.term("end"), syntax.ref("eol") + "yield-stmt" : { + syntax.term("yield"), syntax.optional(syntax.nlook(syntax_eol_fn()), syntax.ref("expr")), syntax.ref("endline") + }, + "control-stmt" : { + syntax.cond_or({syntax.term("break")}, {syntax.term("continue")}), syntax.ref("endline") + }, + "expr-stmt" : { + syntax.ref("expr"), syntax.ref("endline") }, "end-stmt" : { - syntax.term("end"), syntax.ref("eol") + syntax.term("end"), syntax_eol_fn() }, - # === Expressions (nl allows multi-line; nlook(endl) prevents ambiguous continuation) === + # Expression "expr" : { - syntax.ref("basic-expr"), syntax.optional(syntax.term(","), syntax.ref("nl"), syntax.ref("expr")) + syntax.ref("basic-expr"), syntax.optional(syntax.term(","), (syntax_nl_fn())..., syntax.ref("expr")) }, "basic-expr" : {syntax.cond_or( - {syntax.ref("bind-expr"), syntax.term("="), syntax.ref("nl"), syntax.ref("cond-expr")}, - {syntax.ref("cond-expr"), syntax.optional(syntax.ref("asi-op"), syntax.ref("nl"), syntax.ref("basic-expr"))} + {syntax.ref("bind-expr"), syntax.term("="), (syntax_nl_fn())..., syntax.ref("cond-expr")}, + {syntax.ref("cond-expr"), syntax.optional(syntax.ref("asi-op"), (syntax_nl_fn())..., syntax.ref("basic-expr"))} )}, "bind-expr" : { - syntax.term("("), syntax.ref("nl"), syntax.ref("bind-list"), syntax.repeat(syntax.term(","), syntax.ref("nl"), syntax.ref("bind-list")), syntax.ref("nl"), syntax.term(")") + syntax.term("("), (syntax_nl_fn())..., syntax.ref("bind-list"), syntax.repeat(syntax.term(","), (syntax_nl_fn())..., syntax.ref("bind-list")), (syntax_nl_fn())..., syntax.term(")") }, "bind-list" : {syntax.cond_or( {syntax.token("id")}, @@ -613,20 +279,18 @@ function get_fmt_syntax() )}, "async-lambda-expr" : { syntax.term("async"), syntax.term("["), syntax.optional(syntax.ref("capture-list")), syntax.term("]"), - syntax.term("("), syntax.ref("nl"), syntax.optional(syntax.ref("argument-list")), syntax.ref("nl"), syntax.term(")"), + syntax.term("("), (syntax_nl_fn())..., syntax.optional(syntax.ref("argument-list")), (syntax_nl_fn())..., syntax.term(")"), syntax.ref("lambda-body") }, "lambda-expr" : { - syntax.term("["), syntax.optional(syntax.ref("capture-list")), syntax.term("]"), - syntax.term("("), syntax.ref("nl"), syntax.optional(syntax.ref("argument-list")), syntax.ref("nl"), syntax.term(")"), - syntax.ref("lambda-body") + syntax.term("["), syntax.optional(syntax.ref("capture-list")), syntax.term("]"), syntax.term("("), (syntax_nl_fn())..., syntax.optional(syntax.ref("argument-list")), (syntax_nl_fn())..., syntax.term(")"), syntax.ref("lambda-body") }, "capture-list" : { - syntax.optional(syntax.term("=")), syntax.token("id"), syntax.repeat(syntax.term(","), syntax.ref("nl"), syntax.ref("capture-list")) + syntax.optional(syntax.term("=")), syntax.token("id"), syntax.repeat(syntax.term(","), (syntax_nl_fn())..., syntax.ref("capture-list")) }, "argument-list" : {syntax.cond_or( {syntax.term("..."), syntax.token("id")}, - {syntax.optional(syntax.term("=")), syntax.token("id"), syntax.optional(syntax.term(":"), syntax.ref("visit-expr")), syntax.repeat(syntax.term(","), syntax.ref("nl"), syntax.ref("argument-list"))} + {syntax.optional(syntax.term("=")), syntax.token("id"), syntax.optional(syntax.term(":"), syntax.ref("visit-expr")), syntax.repeat(syntax.term(","), (syntax_nl_fn())..., syntax.ref("argument-list"))} )}, "lambda-body" : {syntax.cond_or( {syntax.term("{"), syntax.ref("stmts"), syntax.term("}")}, @@ -638,8 +302,8 @@ function get_fmt_syntax() {syntax.ref("logic-or-expr"), syntax.optional(syntax.ref("cond-postfix"))} )}, "cond-postfix" : {syntax.cond_or( - {syntax.term("?"), syntax.ref("nl"), syntax.ref("value-expr"), syntax.term(":"), syntax.ref("nl"), syntax.ref("cond-expr")}, - {syntax.term(":"), syntax.ref("nl"), syntax.ref("value-expr")} + {syntax.term("?"), (syntax_nl_fn())..., syntax.ref("value-expr"), syntax.term(":"), (syntax_nl_fn())..., syntax.ref("cond-expr")}, + {syntax.term(":"), (syntax_nl_fn())..., syntax.ref("value-expr")} )}, "value-expr" : {syntax.cond_or( {syntax.ref("async-lambda-expr")}, @@ -647,25 +311,25 @@ function get_fmt_syntax() {syntax.ref("logic-or-expr")} )}, "logic-or-expr" : { - syntax.ref("logic-and-expr"), syntax.optional(syntax.cond_or({syntax.term("||")}, {syntax.term("or")}), syntax.ref("nl"), syntax.ref("logic-or-expr")) + syntax.ref("logic-and-expr"), syntax.optional(syntax.cond_or({syntax.term("||")}, {syntax.term("or")}), (syntax_nl_fn())..., syntax.ref("logic-or-expr")) }, "logic-and-expr" : { - syntax.ref("equal-expr"), syntax.optional(syntax.cond_or({syntax.term("&&")}, {syntax.term("and")}), syntax.ref("nl"), syntax.ref("logic-and-expr")) + syntax.ref("equal-expr"), syntax.optional(syntax.cond_or({syntax.term("&&")}, {syntax.term("and")}), (syntax_nl_fn())..., syntax.ref("logic-and-expr")) }, "equal-expr" : { - syntax.ref("relat-expr"), syntax.optional(syntax.cond_or({syntax.term("==")}, {syntax.term("!=")}, {syntax.term("is")}, {syntax.term("not")}), syntax.ref("nl"), syntax.ref("equal-expr")) + syntax.ref("relat-expr"), syntax.optional(syntax.cond_or({syntax.term("==")}, {syntax.term("!=")}, {syntax.term("is")}, {syntax.term("not")}), (syntax_nl_fn())..., syntax.ref("equal-expr")) }, "relat-expr" : { - syntax.ref("add-expr"), syntax.optional(syntax.cond_or({syntax.term(">")}, {syntax.term("<")}, {syntax.term(">=")}, {syntax.term("<=")}), syntax.ref("nl"), syntax.ref("relat-expr")) + syntax.ref("add-expr"), syntax.optional(syntax.cond_or({syntax.term(">")}, {syntax.term("<")}, {syntax.term(">=")}, {syntax.term("<=")}), (syntax_nl_fn())..., syntax.ref("relat-expr")) }, "add-expr" : { - syntax.ref("mul-expr"), syntax.optional(syntax.cond_or({syntax.term("+")}, {syntax.term("-")}), syntax.ref("nl"), syntax.ref("add-expr")) + syntax.ref("mul-expr"), syntax.optional(syntax.cond_or({syntax.term("+")}, {syntax.term("-")}), (syntax_nl_fn())..., syntax.ref("add-expr")) }, "mul-expr" : { - syntax.ref("conv-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.term("*")}, {syntax.term("/")}, {syntax.term("%")}, {syntax.term("^")}), syntax.ref("nl"), syntax.ref("mul-expr")) + syntax.ref("conv-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.term("*")}, {syntax.term("/")}, {syntax.term("%")}, {syntax.term("^")}), (syntax_nl_fn())..., syntax.ref("mul-expr")) }, "conv-expr" : { - syntax.ref("unary-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.term("=>")}, {syntax.term("as")}), syntax.ref("nl"), syntax.ref("visit-expr")) + syntax.ref("unary-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.term("=>")}, {syntax.term("as")}), (syntax_nl_fn())..., syntax.ref("visit-expr")) }, "unary-expr" : {syntax.cond_or( {syntax.ref("unary-op"), syntax.ref("unary-expr")}, @@ -694,7 +358,7 @@ function get_fmt_syntax() {syntax.ref("constant")} )}, "visit-expr" : { - syntax.ref("object"), syntax.optional(syntax.cond_or({syntax.term("->")}, {syntax.term(".")}), syntax.ref("nl"), syntax.ref("visit-expr")) + syntax.ref("object"), syntax.optional(syntax.cond_or({syntax.term("->")}, {syntax.term(".")}), (syntax_nl_fn())..., syntax.ref("visit-expr")) }, "object" : {syntax.cond_or( {syntax.ref("array"), syntax.optional(syntax.ref("index"))}, @@ -706,11 +370,10 @@ function get_fmt_syntax() {syntax.token("char")} )}, "ecsx-extend" : { - syntax.token("id"), syntax.nlook(syntax.token("endl")), syntax.term("::"), syntax.token("id"), - syntax.term("("), syntax.ref("nl"), syntax.optional(syntax.ref("basic-expr")), syntax.ref("nl"), syntax.term(")") + syntax.token("id"), syntax.nlook(syntax.token("endl")), syntax.term("::"), syntax.token("id"), syntax.term("("), (syntax_nl_fn())..., syntax.optional(syntax.ref("basic-expr")), (syntax_nl_fn())..., syntax.term(")") }, "element" : { - syntax.cond_or({syntax.token("id")}, {syntax.term("("), syntax.ref("nl"), syntax.ref("basic-expr"), syntax.ref("nl"), syntax.term(")")}), + syntax.cond_or({syntax.token("id")}, {syntax.term("("), (syntax_nl_fn())..., syntax.ref("basic-expr"), (syntax_nl_fn())..., syntax.term(")")}), syntax.repeat(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.ref("fcall")}, {syntax.ref("index")})) }, "constant" : {syntax.cond_or( @@ -720,54 +383,34 @@ function get_fmt_syntax() {syntax.term("false")} )}, "array" : { - syntax.term("{"), syntax.ref("nl"), syntax.optional(syntax.ref("expr")), syntax.ref("nl"), syntax.term("}") + syntax.term("{"), (syntax_nl_fn())..., syntax.optional(syntax.ref("expr")), (syntax_nl_fn())..., syntax.term("}") }, "fcall" : { - syntax.term("("), syntax.ref("nl"), syntax.optional(syntax.ref("expr")), syntax.ref("nl"), syntax.term(")") + syntax.term("("), (syntax_nl_fn())..., syntax.optional(syntax.ref("expr")), (syntax_nl_fn())..., syntax.term(")") }, "index" : {syntax.cond_or( - {syntax.term("["), syntax.ref("nl"), syntax.optional(syntax.ref("add-expr")), syntax.optional(syntax.term(":"), syntax.ref("nl"), syntax.optional(syntax.ref("add-expr")), syntax.optional(syntax.term(":"), syntax.ref("nl"), syntax.optional(syntax.ref("add-expr")))), syntax.ref("nl"), syntax.term("]")}, + {syntax.term("["), syntax.optional(syntax.ref("add-expr")), syntax.optional(syntax.term(":"), syntax.optional(syntax.ref("add-expr")), syntax.optional(syntax.term(":"), syntax.optional(syntax.ref("add-expr")))), syntax.term("]")}, {syntax.term("["), syntax.term("::"), syntax.term("]")} )} }.to_hash_map() @end -end - -function get_fmt_grammar() - var g = new parsergen.grammar - g.ext = ".*\\.(csp|csc|ecs|ecsx)" - g.lex = get_fmt_lexical(regex.build_optimize) - g.stx := get_fmt_syntax() - return g -end - -# Verify format grammar covers all main grammar rules (call from tests) -function check_fmt_syntax_sync() - var main_stx = covscript_syntax - var fmt_stx = get_fmt_syntax() - var fmt_extra = new hash_map - fmt_extra["nl"] = 1 - fmt_extra["eos"] = 1 - fmt_extra["eol"] = 1 - var main_extra = new hash_map - main_extra["ignore"] = 1 - var missing = new array - foreach it in main_stx - var name = it.first - if !main_extra.exist(name) && !fmt_stx.exist(name) - missing.push_back(name) - end - end - foreach it in fmt_stx - var name = it.first - if !fmt_extra.exist(name) && !main_stx.exist(name) - missing.push_back("fmt-only:" + name) - end + if strict + stx["nl"] = {syntax.repeat(syntax.token("endl"))} + stx["eol"] = {syntax.optional(syntax.token("com")), syntax.token("endl")} + @begin + stx["eos"] = {syntax.cond_or( + {syntax.token("com"), syntax.token("endl")}, + {syntax.token("endl")} + )} + @end + else + # Ignore if not match initiatively + stx["ignore"] = {syntax.repeat(syntax.token("endl"))} end - return missing + return stx end var grammar = new parsergen.grammar grammar.ext = ".*\\.(csp|csc|ecs|ecsx)" -grammar.lex = get_lexical(regex.build_optimize) -grammar.stx := covscript_syntax +grammar.lex = get_lexical(regex.build_optimize, false) +grammar.stx := get_syntax(false) From a28b3bb7a8f1312f85eed196c4a3de273084930d Mon Sep 17 00:00:00 2001 From: Mike Lee Date: Wed, 22 Jul 2026 23:01:31 +0800 Subject: [PATCH 03/13] refactor: streamline syntax handling in get_syntax function for improved readability and maintainability --- imports/ecs_parser.csp | 75 ++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/imports/ecs_parser.csp b/imports/ecs_parser.csp index 84237dc..6f9ba59 100644 --- a/imports/ecs_parser.csp +++ b/imports/ecs_parser.csp @@ -52,22 +52,19 @@ end function get_syntax(strict) var syntax_eol_fn = null var syntax_eos_fn = null - var syntax_nl_fn = null + var syntax_op_fn = null if strict syntax_eol_fn = []()->syntax.ref("eol") syntax_eos_fn = []()->syntax.ref("eos") - syntax_nl_fn = []()->{syntax.ref("nl")} + syntax_op_fn = [](...args)->args else syntax_eol_fn = []()->syntax.token("endl") syntax_eos_fn = []()->syntax.token("endl") - syntax_nl_fn = []()->{} + # No op on syntax tree if not strict + syntax_op_fn = [](...args)->{} end @begin var stx = { - # Helper: newlines within multi-line expressions - # non-strict: ignore eats endl -> nl matches zero (no-op) - # strict: no ignore -> nl consumes visible endl tokens - "nl" : {syntax.repeat(syntax.token("endl"))}, # Beginning of Parsing "begin" : { syntax.ref("stmts") @@ -79,11 +76,11 @@ function get_syntax(strict) )}, # Bootstrap "stmts" : { - syntax.repeat(syntax_eos_fn()), + (syntax_op_fn(syntax.repeat(syntax_eos_fn())))..., syntax.repeat(syntax.nlook(syntax.ref("endblock")), syntax.ref("statement"), syntax.repeat(syntax_eos_fn())) }, "decl-stmts" : { - syntax.repeat(syntax_eos_fn()), + (syntax_op_fn(syntax.repeat(syntax_eos_fn())))..., syntax.repeat(syntax.nlook(syntax.ref("endblock")), syntax.ref("declaration"), syntax.repeat(syntax_eos_fn())) }, "endblock" : {syntax.cond_or( @@ -142,13 +139,13 @@ function get_syntax(strict) syntax.ref("module-list"), syntax.optional(syntax.term("as"), syntax.token("id")), syntax.optional(syntax.term(","), syntax.ref("import-list")) }, "var-def" : { - syntax.cond_or({syntax.ref("var-bind"), syntax.term("="), (syntax_nl_fn())..., syntax.ref("basic-expr")}, {syntax.ref("var-list")}) + syntax.cond_or({syntax.ref("var-bind"), syntax.term("="), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("basic-expr")}, {syntax.ref("var-list")}) }, "var-stmt" : { syntax.cond_or({syntax.term("var")}, {syntax.term("link")}, {syntax.term("constant")}), syntax.ref("var-def"), syntax.ref("endline") }, "var-bind" : { - syntax.term("("), (syntax_nl_fn())..., syntax.ref("var-bind-list"), syntax.repeat(syntax.term(","), (syntax_nl_fn())..., syntax.ref("var-bind-list")), (syntax_nl_fn())..., syntax.term(")") + syntax.term("("), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("var-bind-list"), syntax.repeat(syntax.term(","), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("var-bind-list")), (syntax_op_fn(syntax.ref("nl")))..., syntax.term(")") }, "var-bind-list" : {syntax.cond_or( {syntax.token("id")}, @@ -157,9 +154,9 @@ function get_syntax(strict) )}, "var-list" : { syntax.token("id"), syntax.cond_or( - {syntax.term("="), (syntax_nl_fn())..., syntax.ref("basic-expr")}, + {syntax.term("="), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("basic-expr")}, {syntax.term("as"), syntax.ref("unary-expr"), syntax.optional(syntax.ref("array"))} - ), syntax.optional(syntax.term(","), (syntax_nl_fn())..., syntax.ref("var-list")) + ), syntax.optional(syntax.term(","), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("var-list")) }, "block-stmt" : { syntax.term("block"), syntax_eol_fn(), syntax.ref("stmts"), syntax.term("end"), syntax_eol_fn() @@ -183,7 +180,7 @@ function get_syntax(strict) syntax.term("switch"), syntax.ref("basic-expr"), syntax_eol_fn(), syntax.ref("switch-stmts"), syntax.term("end"), syntax_eol_fn() }, "switch-stmts" : { - syntax.repeat(syntax_eos_fn()), + (syntax_op_fn(syntax.repeat(syntax_eos_fn())))..., syntax.repeat(syntax.cond_or({syntax.ref("switch-case")}, {syntax.ref("switch-default")}), syntax.repeat(syntax_eos_fn())) }, "switch-case" : { @@ -202,7 +199,7 @@ function get_syntax(strict) syntax.term("until"), syntax.ref("basic-expr"), syntax_eol_fn() }, "for-stmt" : { - syntax.term("for"), syntax.optional(syntax.ref("var-def")), syntax.cond_or({syntax.term(";")}, {syntax.term(",")}), (syntax_nl_fn())..., syntax.optional(syntax.ref("basic-expr")), syntax.cond_or({syntax.term(";")}, {syntax.term(",")}), (syntax_nl_fn())..., syntax.optional(syntax.ref("basic-expr")), syntax.ref("for-body") + syntax.term("for"), syntax.optional(syntax.ref("var-def")), syntax.cond_or({syntax.term(";")}, {syntax.term(",")}), (syntax_op_fn(syntax.ref("nl")))..., syntax.optional(syntax.ref("basic-expr")), syntax.cond_or({syntax.term(";")}, {syntax.term(",")}), (syntax_op_fn(syntax.ref("nl")))..., syntax.optional(syntax.ref("basic-expr")), syntax.ref("for-body") }, "foreach-stmt" : { syntax.term("foreach"), syntax.optional(syntax.nlook(syntax.term("in")), syntax.token("id")), syntax.term("in"), syntax.ref("basic-expr"), syntax.ref("for-body") @@ -212,7 +209,7 @@ function get_syntax(strict) {syntax_eol_fn(), syntax.ref("stmts"), syntax.term("end"), syntax_eol_fn()} )}, "function-stmt" : { - syntax.term("function"), syntax.token("id"), syntax.term("("), (syntax_nl_fn())..., syntax.optional(syntax.ref("argument-list")), (syntax_nl_fn())..., syntax.term(")"), syntax.optional(syntax.term("override")), syntax.ref("function-body") + syntax.term("function"), syntax.token("id"), syntax.term("("), (syntax_op_fn(syntax.ref("nl")))..., syntax.optional(syntax.ref("argument-list")), (syntax_op_fn(syntax.ref("nl")))..., syntax.term(")"), syntax.optional(syntax.term("override")), syntax.ref("function-body") }, "function-body" : {syntax.cond_or( {syntax.term("{"), syntax.ref("stmts"), syntax.term("}")}, @@ -236,7 +233,7 @@ function get_syntax(strict) }, "async-function-stmt" : { syntax.term("async"), syntax.term("function"), syntax.token("id"), - syntax.term("("), (syntax_nl_fn())..., syntax.optional(syntax.ref("argument-list")), (syntax_nl_fn())..., syntax.term(")"), + syntax.term("("), (syntax_op_fn(syntax.ref("nl")))..., syntax.optional(syntax.ref("argument-list")), (syntax_op_fn(syntax.ref("nl")))..., syntax.term(")"), syntax.optional(syntax.term("override")), syntax.ref("function-body") }, "yield-stmt" : { @@ -253,14 +250,14 @@ function get_syntax(strict) }, # Expression "expr" : { - syntax.ref("basic-expr"), syntax.optional(syntax.term(","), (syntax_nl_fn())..., syntax.ref("expr")) + syntax.ref("basic-expr"), syntax.optional(syntax.term(","), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("expr")) }, "basic-expr" : {syntax.cond_or( - {syntax.ref("bind-expr"), syntax.term("="), (syntax_nl_fn())..., syntax.ref("cond-expr")}, - {syntax.ref("cond-expr"), syntax.optional(syntax.ref("asi-op"), (syntax_nl_fn())..., syntax.ref("basic-expr"))} + {syntax.ref("bind-expr"), syntax.term("="), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("cond-expr")}, + {syntax.ref("cond-expr"), syntax.optional(syntax.ref("asi-op"), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("basic-expr"))} )}, "bind-expr" : { - syntax.term("("), (syntax_nl_fn())..., syntax.ref("bind-list"), syntax.repeat(syntax.term(","), (syntax_nl_fn())..., syntax.ref("bind-list")), (syntax_nl_fn())..., syntax.term(")") + syntax.term("("), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("bind-list"), syntax.repeat(syntax.term(","), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("bind-list")), (syntax_op_fn(syntax.ref("nl")))..., syntax.term(")") }, "bind-list" : {syntax.cond_or( {syntax.token("id")}, @@ -279,18 +276,18 @@ function get_syntax(strict) )}, "async-lambda-expr" : { syntax.term("async"), syntax.term("["), syntax.optional(syntax.ref("capture-list")), syntax.term("]"), - syntax.term("("), (syntax_nl_fn())..., syntax.optional(syntax.ref("argument-list")), (syntax_nl_fn())..., syntax.term(")"), + syntax.term("("), (syntax_op_fn(syntax.ref("nl")))..., syntax.optional(syntax.ref("argument-list")), (syntax_op_fn(syntax.ref("nl")))..., syntax.term(")"), syntax.ref("lambda-body") }, "lambda-expr" : { - syntax.term("["), syntax.optional(syntax.ref("capture-list")), syntax.term("]"), syntax.term("("), (syntax_nl_fn())..., syntax.optional(syntax.ref("argument-list")), (syntax_nl_fn())..., syntax.term(")"), syntax.ref("lambda-body") + syntax.term("["), syntax.optional(syntax.ref("capture-list")), syntax.term("]"), syntax.term("("), (syntax_op_fn(syntax.ref("nl")))..., syntax.optional(syntax.ref("argument-list")), (syntax_op_fn(syntax.ref("nl")))..., syntax.term(")"), syntax.ref("lambda-body") }, "capture-list" : { - syntax.optional(syntax.term("=")), syntax.token("id"), syntax.repeat(syntax.term(","), (syntax_nl_fn())..., syntax.ref("capture-list")) + syntax.optional(syntax.term("=")), syntax.token("id"), syntax.repeat(syntax.term(","), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("capture-list")) }, "argument-list" : {syntax.cond_or( {syntax.term("..."), syntax.token("id")}, - {syntax.optional(syntax.term("=")), syntax.token("id"), syntax.optional(syntax.term(":"), syntax.ref("visit-expr")), syntax.repeat(syntax.term(","), (syntax_nl_fn())..., syntax.ref("argument-list"))} + {syntax.optional(syntax.term("=")), syntax.token("id"), syntax.optional(syntax.term(":"), syntax.ref("visit-expr")), syntax.repeat(syntax.term(","), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("argument-list"))} )}, "lambda-body" : {syntax.cond_or( {syntax.term("{"), syntax.ref("stmts"), syntax.term("}")}, @@ -302,8 +299,8 @@ function get_syntax(strict) {syntax.ref("logic-or-expr"), syntax.optional(syntax.ref("cond-postfix"))} )}, "cond-postfix" : {syntax.cond_or( - {syntax.term("?"), (syntax_nl_fn())..., syntax.ref("value-expr"), syntax.term(":"), (syntax_nl_fn())..., syntax.ref("cond-expr")}, - {syntax.term(":"), (syntax_nl_fn())..., syntax.ref("value-expr")} + {syntax.term("?"), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("value-expr"), syntax.term(":"), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("cond-expr")}, + {syntax.term(":"), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("value-expr")} )}, "value-expr" : {syntax.cond_or( {syntax.ref("async-lambda-expr")}, @@ -311,25 +308,25 @@ function get_syntax(strict) {syntax.ref("logic-or-expr")} )}, "logic-or-expr" : { - syntax.ref("logic-and-expr"), syntax.optional(syntax.cond_or({syntax.term("||")}, {syntax.term("or")}), (syntax_nl_fn())..., syntax.ref("logic-or-expr")) + syntax.ref("logic-and-expr"), syntax.optional(syntax.cond_or({syntax.term("||")}, {syntax.term("or")}), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("logic-or-expr")) }, "logic-and-expr" : { - syntax.ref("equal-expr"), syntax.optional(syntax.cond_or({syntax.term("&&")}, {syntax.term("and")}), (syntax_nl_fn())..., syntax.ref("logic-and-expr")) + syntax.ref("equal-expr"), syntax.optional(syntax.cond_or({syntax.term("&&")}, {syntax.term("and")}), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("logic-and-expr")) }, "equal-expr" : { - syntax.ref("relat-expr"), syntax.optional(syntax.cond_or({syntax.term("==")}, {syntax.term("!=")}, {syntax.term("is")}, {syntax.term("not")}), (syntax_nl_fn())..., syntax.ref("equal-expr")) + syntax.ref("relat-expr"), syntax.optional(syntax.cond_or({syntax.term("==")}, {syntax.term("!=")}, {syntax.term("is")}, {syntax.term("not")}), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("equal-expr")) }, "relat-expr" : { - syntax.ref("add-expr"), syntax.optional(syntax.cond_or({syntax.term(">")}, {syntax.term("<")}, {syntax.term(">=")}, {syntax.term("<=")}), (syntax_nl_fn())..., syntax.ref("relat-expr")) + syntax.ref("add-expr"), syntax.optional(syntax.cond_or({syntax.term(">")}, {syntax.term("<")}, {syntax.term(">=")}, {syntax.term("<=")}), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("relat-expr")) }, "add-expr" : { - syntax.ref("mul-expr"), syntax.optional(syntax.cond_or({syntax.term("+")}, {syntax.term("-")}), (syntax_nl_fn())..., syntax.ref("add-expr")) + syntax.ref("mul-expr"), syntax.optional(syntax.cond_or({syntax.term("+")}, {syntax.term("-")}), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("add-expr")) }, "mul-expr" : { - syntax.ref("conv-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.term("*")}, {syntax.term("/")}, {syntax.term("%")}, {syntax.term("^")}), (syntax_nl_fn())..., syntax.ref("mul-expr")) + syntax.ref("conv-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.term("*")}, {syntax.term("/")}, {syntax.term("%")}, {syntax.term("^")}), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("mul-expr")) }, "conv-expr" : { - syntax.ref("unary-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.term("=>")}, {syntax.term("as")}), (syntax_nl_fn())..., syntax.ref("visit-expr")) + syntax.ref("unary-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.term("=>")}, {syntax.term("as")}), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("visit-expr")) }, "unary-expr" : {syntax.cond_or( {syntax.ref("unary-op"), syntax.ref("unary-expr")}, @@ -358,7 +355,7 @@ function get_syntax(strict) {syntax.ref("constant")} )}, "visit-expr" : { - syntax.ref("object"), syntax.optional(syntax.cond_or({syntax.term("->")}, {syntax.term(".")}), (syntax_nl_fn())..., syntax.ref("visit-expr")) + syntax.ref("object"), syntax.optional(syntax.cond_or({syntax.term("->")}, {syntax.term(".")}), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("visit-expr")) }, "object" : {syntax.cond_or( {syntax.ref("array"), syntax.optional(syntax.ref("index"))}, @@ -370,10 +367,10 @@ function get_syntax(strict) {syntax.token("char")} )}, "ecsx-extend" : { - syntax.token("id"), syntax.nlook(syntax.token("endl")), syntax.term("::"), syntax.token("id"), syntax.term("("), (syntax_nl_fn())..., syntax.optional(syntax.ref("basic-expr")), (syntax_nl_fn())..., syntax.term(")") + syntax.token("id"), syntax.nlook(syntax.token("endl")), syntax.term("::"), syntax.token("id"), syntax.term("("), (syntax_op_fn(syntax.ref("nl")))..., syntax.optional(syntax.ref("basic-expr")), (syntax_op_fn(syntax.ref("nl")))..., syntax.term(")") }, "element" : { - syntax.cond_or({syntax.token("id")}, {syntax.term("("), (syntax_nl_fn())..., syntax.ref("basic-expr"), (syntax_nl_fn())..., syntax.term(")")}), + syntax.cond_or({syntax.token("id")}, {syntax.term("("), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("basic-expr"), (syntax_op_fn(syntax.ref("nl")))..., syntax.term(")")}), syntax.repeat(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.ref("fcall")}, {syntax.ref("index")})) }, "constant" : {syntax.cond_or( @@ -383,10 +380,10 @@ function get_syntax(strict) {syntax.term("false")} )}, "array" : { - syntax.term("{"), (syntax_nl_fn())..., syntax.optional(syntax.ref("expr")), (syntax_nl_fn())..., syntax.term("}") + syntax.term("{"), (syntax_op_fn(syntax.ref("nl")))..., syntax.optional(syntax.ref("expr")), (syntax_op_fn(syntax.ref("nl")))..., syntax.term("}") }, "fcall" : { - syntax.term("("), (syntax_nl_fn())..., syntax.optional(syntax.ref("expr")), (syntax_nl_fn())..., syntax.term(")") + syntax.term("("), (syntax_op_fn(syntax.ref("nl")))..., syntax.optional(syntax.ref("expr")), (syntax_op_fn(syntax.ref("nl")))..., syntax.term(")") }, "index" : {syntax.cond_or( {syntax.term("["), syntax.optional(syntax.ref("add-expr")), syntax.optional(syntax.term(":"), syntax.optional(syntax.ref("add-expr")), syntax.optional(syntax.term(":"), syntax.optional(syntax.ref("add-expr")))), syntax.term("]")}, From 99615d02939a8c32aa93714366c6999775e7cd70 Mon Sep 17 00:00:00 2001 From: Mike Lee Date: Wed, 22 Jul 2026 23:56:26 +0800 Subject: [PATCH 04/13] feat: add dry-run option for formatting and enhance linter test coverage --- imports/ecs.csp | 11 ++++++++++- imports/ecs_bootstrap.csp | 20 +++++++++++++++----- imports/ecs_format.csp | 4 ++-- imports/ecs_generator.csp | 20 +++++++++++--------- unit_tests/{ => fixtures}/test_lint.ecs | 5 ++++- 5 files changed, 42 insertions(+), 18 deletions(-) rename unit_tests/{ => fixtures}/test_lint.ecs (97%) diff --git a/imports/ecs.csp b/imports/ecs.csp index e3765fc..4198da5 100644 --- a/imports/ecs.csp +++ b/imports/ecs.csp @@ -27,6 +27,11 @@ package ecs # Type System # ============================================================ +struct lambda_base + function construct() + # Override in derived struct. + end +end namespace type_validator function __type(obj) @@ -43,7 +48,11 @@ namespace type_validator end function __function(obj) link real_type = sdk.typeids.get_real(obj) - return real_type == sdk.typeids.callable || real_type == sdk.typeids.memberfn + if real_type == sdk.typeids.callable || real_type == sdk.typeids.memberfn + return true + else + return global.is_a(typeid obj, typeid lambda_base) + end end function __exception(obj) return sdk.typeids.get_real(obj) == sdk.typeids.exception diff --git a/imports/ecs_bootstrap.csp b/imports/ecs_bootstrap.csp index ac2e588..44ab204 100644 --- a/imports/ecs_bootstrap.csp +++ b/imports/ecs_bootstrap.csp @@ -157,6 +157,7 @@ function show_help() " -c Check grammar only\n" + " -l Lint check\n" + " -F Format source file\n" + + " -n Dry-run format (print to stdout)\n" + " -g Generate cSYM info\n" + " -d Run debugger\n" + " -o Set output path\n" + @@ -193,6 +194,7 @@ var csym = false var repl = false var lint_only = false var format_only = false +var format_dry_run = false function process_args(cmd_args) var index = 1 @@ -226,6 +228,9 @@ function process_args(cmd_args) format_only = true no_run = true end + case "-n" + format_dry_run = true + end case "-s" silent = true end @@ -398,6 +403,7 @@ function run_ecs(cmd_args) repl = false lint_only = false format_only = false + format_dry_run = false exit_code = -1 process_args(cmd_args) @@ -466,13 +472,17 @@ function run_ecs(cmd_args) if unicode != null cvt = parser.unicode_cvt end - var output = ecs_format.format_file(file_name, cvt) - if output == null + var formatted = ecs_format.format_file(file_name, cvt) + if formatted == null return 1 end - var ofs = iostream.ofstream(file_name) - ofs.print(output) - system.out.println(file_name + ": formatted.") + if format_dry_run + system.out.print(formatted) + else + var ofs = iostream.ofstream(file_name) + ofs.print(formatted) + system.out.println(file_name + ": formatted.") + end return 0 end parser.add_grammar("ecs-lang", ecs_parser.grammar) diff --git a/imports/ecs_format.csp b/imports/ecs_format.csp index 0f652f0..2fb1622 100644 --- a/imports/ecs_format.csp +++ b/imports/ecs_format.csp @@ -50,8 +50,8 @@ class format_writer return false end var c = to_integer(text[0]) - # A-Z, a-z, _, CJK Unified Ideographs U+4E00..U+9FFF - return (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c == 95 || (c >= 19968 && c <= 40959) + # A-Z, a-z, _, CJK Unified Ideographs, Extension A, Compatibility + return (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c == 95 || (c >= 13312 && c <= 40959) || (c >= 63744 && c <= 64255) end function write(text) diff --git a/imports/ecs_generator.csp b/imports/ecs_generator.csp index 2024652..b99187b 100644 --- a/imports/ecs_generator.csp +++ b/imports/ecs_generator.csp @@ -1017,15 +1017,17 @@ class generator function visit_lambda_body(nodes) var idx = 0 if typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "{" - # Brace body: { stmts... } + # Brace body: { stmts } ++idx - while idx < nodes.size && (typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "statement") - if stack.front.body_stmts == null - stack.front.body_stmts = new array - end - stack.front.body_stmts.push_back(nodes[idx++].nodes) - while idx < nodes.size && (typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].type == "endl") - ++idx + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "stmts" + var stmts_nodes = nodes[idx].nodes + for i = 0, i < stmts_nodes.size, ++i + if typeid stmts_nodes[i] == typeid parsergen.syntax_tree && stmts_nodes[i].root == "statement" + if stack.front.body_stmts == null + stack.front.body_stmts = new array + end + stack.front.body_stmts.push_back(stmts_nodes[i].nodes) + end end end else @@ -2529,7 +2531,7 @@ class generator print_indent() println("end", dat.pos) else - println("struct __" + ecs_prefix + "ecs_lambda_impl_" + to_string(lambda_base + i + 1) + "__", dat.pos) + println("struct __" + ecs_prefix + "ecs_lambda_impl_" + to_string(lambda_base + i + 1) + "__ extends " + ecs_prefix + "ecs.lambda_base", dat.pos) foreach it in dat.capture_list print_indent() println("var " + it.first + " = null", dat.pos) diff --git a/unit_tests/test_lint.ecs b/unit_tests/fixtures/test_lint.ecs similarity index 97% rename from unit_tests/test_lint.ecs rename to unit_tests/fixtures/test_lint.ecs index b9541a8..329e199 100644 --- a/unit_tests/test_lint.ecs +++ b/unit_tests/fixtures/test_lint.ecs @@ -17,8 +17,11 @@ end if true # empty end -var trailing = 1 +var trailing = 1 # === Consecutive blank lines (intentional) === + + + var a = 1 var b = 2 # === Useless expression === From 6a394401b67de971ea93f297e22979f802c38609 Mon Sep 17 00:00:00 2001 From: Mike Lee Date: Wed, 22 Jul 2026 23:59:30 +0800 Subject: [PATCH 05/13] feat: add unit tests for linter rules and parser modes to enhance coverage --- imports/ecs.csp | 7 +- {unit_tests/fixtures => tests}/test_lint.ecs | 0 unit_tests/test_lint_rules.ecs | 127 +++++++++++++ unit_tests/test_parser_modes.ecs | 186 +++++++++++++++++++ 4 files changed, 317 insertions(+), 3 deletions(-) rename {unit_tests/fixtures => tests}/test_lint.ecs (100%) create mode 100644 unit_tests/test_lint_rules.ecs create mode 100644 unit_tests/test_parser_modes.ecs diff --git a/imports/ecs.csp b/imports/ecs.csp index 4198da5..3c7e76b 100644 --- a/imports/ecs.csp +++ b/imports/ecs.csp @@ -28,9 +28,10 @@ package ecs # ============================================================ struct lambda_base - function construct() - # Override in derived struct. - end + # This is an empty base struct used to identify lambda objects + # in the CovScript type system. It serves as a marker for lambda + # functions and allows for type checking and validation of lambda + # objects within the ECS framework. end namespace type_validator diff --git a/unit_tests/fixtures/test_lint.ecs b/tests/test_lint.ecs similarity index 100% rename from unit_tests/fixtures/test_lint.ecs rename to tests/test_lint.ecs diff --git a/unit_tests/test_lint_rules.ecs b/unit_tests/test_lint_rules.ecs new file mode 100644 index 0000000..c8afe68 --- /dev/null +++ b/unit_tests/test_lint_rules.ecs @@ -0,0 +1,127 @@ +# Unit tests: Lint rules automated verification +import ecs_lint + +var passed = 0 +var failed = 0 + +function assert(condition, msg) + if condition + ++passed + else + system.out.println("FAIL: " + msg) + ++failed + end +end + +function assert_eq(actual, expected, msg) + if actual == expected + ++passed + else + system.out.println("FAIL: " + msg + " -- expected " + to_string(expected) + ", got " + to_string(actual)) + ++failed + end +end + +function assert_ge(actual, min_expected, msg) + if actual >= min_expected + ++passed + else + system.out.println("FAIL: " + msg + " -- expected >= " + to_string(min_expected) + ", got " + to_string(actual)) + ++failed + end +end + +var result = ecs_lint.lint_file("unit_tests/fixtures/test_lint.ecs") +assert(result != null, "lint_file returns non-null result") + +var lint = result[0] +var issues = result[1] + +assert(typeid lint == typeid ecs_lint.linter, "result[0] is a linter") +assert(typeid issues == typeid array, "result[1] is an array (issues)") + +# Count issues by rule +var function_naming_count = 0 +var variable_naming_count = 0 +var constant_naming_count = 0 +var class_naming_count = 0 +var unreachable_code_count = 0 +var empty_block_count = 0 +var trailing_whitespace_count = 0 +var consecutive_blank_lines_count = 0 +var indentation_count = 0 +var useless_expression_count = 0 +var error_count = 0 +var warning_count = 0 +var info_count = 0 + +foreach iss in issues + if iss.rule_name == "function_naming" + ++function_naming_count + end + if iss.rule_name == "variable_naming" + ++variable_naming_count + end + if iss.rule_name == "constant_naming" + ++constant_naming_count + end + if iss.rule_name == "class_naming" + ++class_naming_count + end + if iss.rule_name == "unreachable_code" + ++unreachable_code_count + end + if iss.rule_name == "empty_block" + ++empty_block_count + end + if iss.rule_name == "trailing_whitespace" + ++trailing_whitespace_count + end + if iss.rule_name == "consecutive_blank_lines" + ++consecutive_blank_lines_count + end + if iss.rule_name == "indentation_consistency" + ++indentation_count + end + if iss.rule_name == "useless_expression" + ++useless_expression_count + end + if iss.severity == ecs_lint.SEVERITY_ERROR + ++error_count + end + if iss.severity == ecs_lint.SEVERITY_WARNING + ++warning_count + end + if iss.severity == ecs_lint.SEVERITY_INFO + ++info_count + end +end + +# Verify expected lint issues +assert_ge(function_naming_count, 1, "detects function_naming violations") +assert_ge(variable_naming_count, 1, "detects variable_naming violations") +assert_ge(constant_naming_count, 1, "detects constant_naming violations") +assert_ge(class_naming_count, 1, "detects class_naming violations") +assert_ge(unreachable_code_count, 1, "detects unreachable_code errors") +assert_ge(empty_block_count, 1, "detects empty_block warnings") +assert_ge(trailing_whitespace_count, 1, "detects trailing_whitespace warnings") +assert_ge(consecutive_blank_lines_count, 1, "detects consecutive_blank_lines info") + +# Verify severity counts via linter method +assert_eq(lint.count_by_severity(ecs_lint.SEVERITY_ERROR), error_count, "count_by_severity(error) matches") +assert_eq(lint.count_by_severity(ecs_lint.SEVERITY_WARNING), warning_count, "count_by_severity(warning) matches") +assert_eq(lint.count_by_severity(ecs_lint.SEVERITY_INFO), info_count, "count_by_severity(info) matches") + +# Verify error/warning counts match issue array +assert_ge(error_count, 1, "has at least one severity_error") +assert_ge(warning_count, 5, "has at least five severity_warning") + +# Verify total issue count +assert_ge(issues.size, 6, "total issues >= 6") + +# === results === +system.out.println("") +system.out.println("Test: lint rules -- " + to_string(passed) + " passed, " + to_string(failed) + " failed") +if failed > 0 + system.exit(1) +end diff --git a/unit_tests/test_parser_modes.ecs b/unit_tests/test_parser_modes.ecs new file mode 100644 index 0000000..b82bdc2 --- /dev/null +++ b/unit_tests/test_parser_modes.ecs @@ -0,0 +1,186 @@ +# Unit tests: Parser strict/non-strict mode coverage +import ecs +import ecs_parser, parsergen, regex + +var passed = 0 +var failed = 0 + +function assert(condition, msg) + if condition + ++passed + else + system.out.println("FAIL: " + msg) + ++failed + end +end + +# === Lexical: non-strict mode === +var lex_ns = ecs_parser.get_lexical(regex.build_optimize, false) +assert(typeid lex_ns == typeid hash_map, "non-strict lex is hash_map") + +# Non-strict: "ign" key must exist, "com" must not +var keys_ns = new array +foreach pair in lex_ns + keys_ns.push_back(pair.key) +end + +var has_ign_ns = false +var has_com_ns = false +foreach k in keys_ns + if k == "ign" + has_ign_ns = true + end + if k == "com" + has_com_ns = true + end +end +assert(has_ign_ns, "non-strict lex has 'ign'") +assert(!has_com_ns, "non-strict lex does not have 'com'") + +# === Lexical: strict mode === +var lex_s = ecs_parser.get_lexical(regex.build_optimize, true) +assert(typeid lex_s == typeid hash_map, "strict lex is hash_map") + +var keys_s = new array +foreach pair in lex_s + keys_s.push_back(pair.key) +end + +var has_ign_s = false +var has_com_s = false +foreach k in keys_s + if k == "ign" + has_ign_s = true + end + if k == "com" + has_com_s = true + end +end +assert(has_ign_s, "strict lex has 'ign'") +assert(has_com_s, "strict lex has 'com'") + +# === Syntax: non-strict mode === +var stx_ns = ecs_parser.get_syntax(false) +assert(typeid stx_ns == typeid hash_map, "non-strict stx is hash_map") + +# Non-strict: must have "ignore", must not have "nl" / "eol" / "eos" +var stx_keys_ns = new array +foreach pair in stx_ns + stx_keys_ns.push_back(pair.key) +end + +var has_ignore_ns = false +var has_nl_ns = false +var has_eol_ns = false +var has_eos_ns = false +foreach k in stx_keys_ns + if k == "ignore" + has_ignore_ns = true + end + if k == "nl" + has_nl_ns = true + end + if k == "eol" + has_eol_ns = true + end + if k == "eos" + has_eos_ns = true + end +end +assert(has_ignore_ns, "non-strict stx has 'ignore'") +assert(!has_nl_ns, "non-strict stx does not have 'nl'") +assert(!has_eol_ns, "non-strict stx does not have 'eol'") +assert(!has_eos_ns, "non-strict stx does not have 'eos'") + +# Must have main rule names +var has_begin_ns = false +var has_stmts_ns = false +var has_statement_ns = false +foreach k in stx_keys_ns + if k == "begin" + has_begin_ns = true + end + if k == "stmts" + has_stmts_ns = true + end + if k == "statement" + has_statement_ns = true + end +end +assert(has_begin_ns, "non-strict stx has 'begin'") +assert(has_stmts_ns, "non-strict stx has 'stmts'") +assert(has_statement_ns, "non-strict stx has 'statement'") + +# === Syntax: strict mode === +var stx_s = ecs_parser.get_syntax(true) +assert(typeid stx_s == typeid hash_map, "strict stx is hash_map") + +var stx_keys_s = new array +foreach pair in stx_s + stx_keys_s.push_back(pair.key) +end + +var has_ignore_s = false +var has_nl_s = false +var has_eol_s = false +var has_eos_s = false +foreach k in stx_keys_s + if k == "ignore" + has_ignore_s = true + end + if k == "nl" + has_nl_s = true + end + if k == "eol" + has_eol_s = true + end + if k == "eos" + has_eos_s = true + end +end +assert(!has_ignore_s, "strict stx does not have 'ignore'") +assert(has_nl_s, "strict stx has 'nl'") +assert(has_eol_s, "strict stx has 'eol'") +assert(has_eos_s, "strict stx has 'eos'") + +var has_begin_s = false +var has_stmts_s = false +foreach k in stx_keys_s + if k == "begin" + has_begin_s = true + end + if k == "stmts" + has_stmts_s = true + end +end +assert(has_begin_s, "strict stx has 'begin'") +assert(has_stmts_s, "strict stx has 'stmts'") + +# === Parse with non-strict grammar (default) === +var grammar_ns = new parsergen.grammar +grammar_ns.ext = ".*\\.(csp|csc|ecs|ecsx)" +grammar_ns.lex = ecs_parser.get_lexical(regex.build_optimize, false) +grammar_ns.stx := ecs_parser.get_syntax(false) + +var parser_ns = new parsergen.generator +parser_ns.add_grammar("ecs-lang-ns", grammar_ns) +parser_ns.from_file("unit_tests/test_functions.ecs") +assert(parser_ns.ast != null, "non-strict mode parses test_functions.ecs") + +# === Parse with strict grammar === +var grammar_s = new parsergen.grammar +grammar_s.ext = ".*\\.(csp|csc|ecs|ecsx)" +grammar_s.lex = ecs_parser.get_lexical(regex.build_optimize, true) +grammar_s.stx := ecs_parser.get_syntax(true) + +var parser_s = new parsergen.generator +parser_s.add_grammar("ecs-lang-s", grammar_s) +parser_s.from_file("unit_tests/test_functions.ecs") +assert(parser_s.ast != null, "strict mode parses test_functions.ecs") + +# === results === +system.out.println("") +system.out.println("Test: parser modes -- " + to_string(passed) + " passed, " + to_string(failed) + " failed") +if failed > 0 + system.exit(1) +end From 332ce75d65e082c6718023e9a383756250595e04 Mon Sep 17 00:00:00 2001 From: Mike Lee Date: Thu, 23 Jul 2026 00:10:56 +0800 Subject: [PATCH 06/13] refactor: simplify lint rule assertions and enhance parser mode tests --- unit_tests/test_lint_rules.ecs | 16 ++-- unit_tests/test_parser_modes.ecs | 156 ++++++------------------------- 2 files changed, 37 insertions(+), 135 deletions(-) diff --git a/unit_tests/test_lint_rules.ecs b/unit_tests/test_lint_rules.ecs index c8afe68..0e3b321 100644 --- a/unit_tests/test_lint_rules.ecs +++ b/unit_tests/test_lint_rules.ecs @@ -31,8 +31,15 @@ function assert_ge(actual, min_expected, msg) end end -var result = ecs_lint.lint_file("unit_tests/fixtures/test_lint.ecs") +var result = ecs_lint.lint_file("tests/test_lint.ecs") assert(result != null, "lint_file returns non-null result") +if result == null + system.out.println("Test: lint rules -- " + to_string(passed) + " passed, " + to_string(failed) + " failed") + if failed > 0 + system.exit(1) + end + return +end var lint = result[0] var issues = result[1] @@ -40,7 +47,6 @@ var issues = result[1] assert(typeid lint == typeid ecs_lint.linter, "result[0] is a linter") assert(typeid issues == typeid array, "result[1] is an array (issues)") -# Count issues by rule var function_naming_count = 0 var variable_naming_count = 0 var constant_naming_count = 0 @@ -97,7 +103,6 @@ foreach iss in issues end end -# Verify expected lint issues assert_ge(function_naming_count, 1, "detects function_naming violations") assert_ge(variable_naming_count, 1, "detects variable_naming violations") assert_ge(constant_naming_count, 1, "detects constant_naming violations") @@ -107,19 +112,14 @@ assert_ge(empty_block_count, 1, "detects empty_block warnings") assert_ge(trailing_whitespace_count, 1, "detects trailing_whitespace warnings") assert_ge(consecutive_blank_lines_count, 1, "detects consecutive_blank_lines info") -# Verify severity counts via linter method assert_eq(lint.count_by_severity(ecs_lint.SEVERITY_ERROR), error_count, "count_by_severity(error) matches") assert_eq(lint.count_by_severity(ecs_lint.SEVERITY_WARNING), warning_count, "count_by_severity(warning) matches") assert_eq(lint.count_by_severity(ecs_lint.SEVERITY_INFO), info_count, "count_by_severity(info) matches") -# Verify error/warning counts match issue array assert_ge(error_count, 1, "has at least one severity_error") assert_ge(warning_count, 5, "has at least five severity_warning") - -# Verify total issue count assert_ge(issues.size, 6, "total issues >= 6") -# === results === system.out.println("") system.out.println("Test: lint rules -- " + to_string(passed) + " passed, " + to_string(failed) + " failed") if failed > 0 diff --git a/unit_tests/test_parser_modes.ecs b/unit_tests/test_parser_modes.ecs index b82bdc2..714501c 100644 --- a/unit_tests/test_parser_modes.ecs +++ b/unit_tests/test_parser_modes.ecs @@ -14,147 +14,49 @@ function assert(condition, msg) end end -# === Lexical: non-strict mode === -var lex_ns = ecs_parser.get_lexical(regex.build_optimize, false) -assert(typeid lex_ns == typeid hash_map, "non-strict lex is hash_map") - -# Non-strict: "ign" key must exist, "com" must not -var keys_ns = new array -foreach pair in lex_ns - keys_ns.push_back(pair.key) -end - -var has_ign_ns = false -var has_com_ns = false -foreach k in keys_ns - if k == "ign" - has_ign_ns = true - end - if k == "com" - has_com_ns = true +function key_exists(map, key) + try + var val = map.at(key) + return true + catch e + return false end end -assert(has_ign_ns, "non-strict lex has 'ign'") -assert(!has_com_ns, "non-strict lex does not have 'com'") -# === Lexical: strict mode === +# === Lexical returns valid hash_maps === +var lex_ns = ecs_parser.get_lexical(regex.build_optimize, false) +assert(typeid lex_ns == typeid hash_map, "non-strict lex is hash_map") + var lex_s = ecs_parser.get_lexical(regex.build_optimize, true) assert(typeid lex_s == typeid hash_map, "strict lex is hash_map") -var keys_s = new array -foreach pair in lex_s - keys_s.push_back(pair.key) -end +# Non-strict: "ign" must exist, "com" must not +assert(key_exists(lex_ns, "ign"), "non-strict lex has 'ign'") +assert(!key_exists(lex_ns, "com"), "non-strict lex does not have 'com'") -var has_ign_s = false -var has_com_s = false -foreach k in keys_s - if k == "ign" - has_ign_s = true - end - if k == "com" - has_com_s = true - end -end -assert(has_ign_s, "strict lex has 'ign'") -assert(has_com_s, "strict lex has 'com'") +# Strict: both "ign" and "com" must exist +assert(key_exists(lex_s, "ign"), "strict lex has 'ign'") +assert(key_exists(lex_s, "com"), "strict lex has 'com'") -# === Syntax: non-strict mode === +# === Syntax returns valid hash_maps === var stx_ns = ecs_parser.get_syntax(false) assert(typeid stx_ns == typeid hash_map, "non-strict stx is hash_map") -# Non-strict: must have "ignore", must not have "nl" / "eol" / "eos" -var stx_keys_ns = new array -foreach pair in stx_ns - stx_keys_ns.push_back(pair.key) -end - -var has_ignore_ns = false -var has_nl_ns = false -var has_eol_ns = false -var has_eos_ns = false -foreach k in stx_keys_ns - if k == "ignore" - has_ignore_ns = true - end - if k == "nl" - has_nl_ns = true - end - if k == "eol" - has_eol_ns = true - end - if k == "eos" - has_eos_ns = true - end -end -assert(has_ignore_ns, "non-strict stx has 'ignore'") -assert(!has_nl_ns, "non-strict stx does not have 'nl'") -assert(!has_eol_ns, "non-strict stx does not have 'eol'") -assert(!has_eos_ns, "non-strict stx does not have 'eos'") - -# Must have main rule names -var has_begin_ns = false -var has_stmts_ns = false -var has_statement_ns = false -foreach k in stx_keys_ns - if k == "begin" - has_begin_ns = true - end - if k == "stmts" - has_stmts_ns = true - end - if k == "statement" - has_statement_ns = true - end -end -assert(has_begin_ns, "non-strict stx has 'begin'") -assert(has_stmts_ns, "non-strict stx has 'stmts'") -assert(has_statement_ns, "non-strict stx has 'statement'") - -# === Syntax: strict mode === var stx_s = ecs_parser.get_syntax(true) assert(typeid stx_s == typeid hash_map, "strict stx is hash_map") -var stx_keys_s = new array -foreach pair in stx_s - stx_keys_s.push_back(pair.key) -end - -var has_ignore_s = false -var has_nl_s = false -var has_eol_s = false -var has_eos_s = false -foreach k in stx_keys_s - if k == "ignore" - has_ignore_s = true - end - if k == "nl" - has_nl_s = true - end - if k == "eol" - has_eol_s = true - end - if k == "eos" - has_eos_s = true - end -end -assert(!has_ignore_s, "strict stx does not have 'ignore'") -assert(has_nl_s, "strict stx has 'nl'") -assert(has_eol_s, "strict stx has 'eol'") -assert(has_eos_s, "strict stx has 'eos'") - -var has_begin_s = false -var has_stmts_s = false -foreach k in stx_keys_s - if k == "begin" - has_begin_s = true - end - if k == "stmts" - has_stmts_s = true - end -end -assert(has_begin_s, "strict stx has 'begin'") -assert(has_stmts_s, "strict stx has 'stmts'") +# Non-strict: must have "ignore" and main rules +assert(key_exists(stx_ns, "ignore"), "non-strict stx has 'ignore'") +assert(key_exists(stx_ns, "begin"), "non-strict stx has 'begin'") +assert(key_exists(stx_ns, "stmts"), "non-strict stx has 'stmts'") +assert(key_exists(stx_ns, "statement"), "non-strict stx has 'statement'") + +# Strict: must have "nl", "eol", "eos" and main rules +assert(key_exists(stx_s, "nl"), "strict stx has 'nl'") +assert(key_exists(stx_s, "eol"), "strict stx has 'eol'") +assert(key_exists(stx_s, "eos"), "strict stx has 'eos'") +assert(key_exists(stx_s, "begin"), "strict stx has 'begin'") +assert(key_exists(stx_s, "stmts"), "strict stx has 'stmts'") # === Parse with non-strict grammar (default) === var grammar_ns = new parsergen.grammar From 14b329bf707a77d82dcea4596129d8194f83baf0 Mon Sep 17 00:00:00 2001 From: Mike Lee Date: Thu, 23 Jul 2026 00:30:26 +0800 Subject: [PATCH 07/13] feat: enhance linter and formatter functionality with improved error handling and code structure --- imports/ecs_bootstrap.csp | 4 +++ imports/ecs_format.csp | 70 ++++++++++++++------------------------- imports/ecs_generator.csp | 15 +++++---- imports/ecs_lint.csp | 43 +++--------------------- 4 files changed, 41 insertions(+), 91 deletions(-) diff --git a/imports/ecs_bootstrap.csp b/imports/ecs_bootstrap.csp index 44ab204..9745a3b 100644 --- a/imports/ecs_bootstrap.csp +++ b/imports/ecs_bootstrap.csp @@ -229,6 +229,8 @@ function process_args(cmd_args) no_run = true end case "-n" + format_only = true + no_run = true format_dry_run = true end case "-s" @@ -463,6 +465,8 @@ function run_ecs(cmd_args) var result = ecs_lint.lint_file(file_name) if result != null result[0].print_report() + else + return 1 end return 0 end diff --git a/imports/ecs_format.csp b/imports/ecs_format.csp index 2fb1622..d9b34d4 100644 --- a/imports/ecs_format.csp +++ b/imports/ecs_format.csp @@ -20,7 +20,7 @@ package ecs_format -import parsergen, ecs_parser, ecs_lint, regex +import parsergen, ecs_parser, regex function get_fmt_grammar() var g = new parsergen.grammar @@ -120,9 +120,6 @@ class format_writer function pop_indent() --indent_level - if indent_level < 0 - indent_level = 0 - end end function token(tok) @@ -144,14 +141,9 @@ end class format_visitor var w = null - var ctx = null function run(file_name, code_buff, ast) - this.ctx = new ecs_lint.lint_context - this.ctx.file_name = file_name - this.ctx.code_buff = code_buff this.w = new format_writer - # Walk AST (comments are now first-class "com" tokens) this.visit_begin(ast.nodes) return this.w.output end @@ -166,9 +158,10 @@ class format_visitor # Standalone comment inside expression (edge case) this.w.write(node.data) this.w.newline() - end - if node.type != "endl" - this.w.token(node) + else + if node.type != "endl" + this.w.token(node) + end end end if typeid node == typeid parsergen.syntax_tree @@ -398,6 +391,9 @@ class format_visitor # === Top-level === function visit_begin(nodes) + if nodes.empty() + return + end # stmts is the first/only child of begin for idx = 0, idx < nodes[0].nodes.size, ++idx var node = nodes[0].nodes[idx] @@ -469,21 +465,21 @@ class format_visitor end function visit_statement(nodes) + if nodes.empty() + return + end var node = nodes[0] if typeid node == typeid parsergen.syntax_tree - if node.root == "if-stmt" || node.root == "while-stmt" || node.root == "for-stmt" || node.root == "foreach-stmt" || node.root == "loop-stmt" || node.root == "function-stmt" || node.root == "async-function-stmt" || node.root == "try-stmt" - ecs_lint.rule_empty_block(this.ctx, node.nodes) - end this.dispatch(node) end end function visit_declaration(nodes) + if nodes.empty() + return + end var node = nodes[0] if typeid node == typeid parsergen.syntax_tree - if node.root == "function-stmt" || node.root == "async-function-stmt" || node.root == "class-stmt" - ecs_lint.rule_empty_block(this.ctx, node.nodes) - end this.dispatch(node) end end @@ -763,9 +759,12 @@ class format_visitor # === Declarations === - function visit_function_stmt(nodes) - ecs_lint.rule_function_naming(this.ctx, nodes) + function visit_function_stmt_impl(nodes, is_async) var idx = 0 + if is_async + ++idx; this.w.keyword("async") + this.w.space() + end ++idx; this.w.keyword("function") this.w.space() this.w.write(nodes[idx++].data) @@ -788,30 +787,12 @@ class format_visitor this.visit_function_body(nodes[idx++].nodes) end + function visit_function_stmt(nodes) + this.visit_function_stmt_impl(nodes, false) + end + function visit_async_function_stmt(nodes) - ecs_lint.rule_async_function_naming(this.ctx, nodes) - var idx = 0 - ++idx; this.w.keyword("async") - this.w.space() - ++idx; this.w.keyword("function") - this.w.space() - this.w.write(nodes[idx++].data) - ++idx; this.w.write("(") - if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" - ++idx - end - if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "argument-list" - this.visit_children(nodes[idx++].nodes) - end - if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" - ++idx - end - ++idx; this.w.write(")") - if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "override" - this.w.space() - ++idx; this.w.write("override") - end - this.visit_function_body(nodes[idx++].nodes) + this.visit_function_stmt_impl(nodes, true) end function visit_function_body(nodes) @@ -847,7 +828,6 @@ class format_visitor end function visit_class_stmt(nodes) - ecs_lint.rule_class_naming(this.ctx, nodes) var idx = 0 if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type this.w.keyword(nodes[idx++].data) @@ -886,7 +866,6 @@ class format_visitor end function visit_var_stmt(nodes) - ecs_lint.rule_variable_naming(this.ctx, nodes) var idx = 0 if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type this.w.keyword(nodes[idx++].data) @@ -938,7 +917,6 @@ class format_visitor function visit_expr_stmt(nodes) var idx = 0 - ecs_lint.rule_useless_expression(this.ctx, nodes) this.visit_children(nodes[idx++].nodes) this.visit_endline(nodes[idx++].nodes) end diff --git a/imports/ecs_generator.csp b/imports/ecs_generator.csp index b99187b..5c2a3c8 100644 --- a/imports/ecs_generator.csp +++ b/imports/ecs_generator.csp @@ -2622,6 +2622,7 @@ class generator end function repl_run(ast) slice_ext = false + lambda_base = 0 indent = -1 recursion_depth = 0 async_depth = 0 @@ -2632,14 +2633,16 @@ class generator try visit_begin(ast.nodes) catch e + lambda_list = new array + stack = new array + indent = -1 + recursion_depth = 0 + async_depth = 0 + in_struct_depth = 0 if e.what == "ECS_ERROR" - lambda_list = new array - stack = new array - indent = -1 - recursion_depth = 0 - async_depth = 0 - in_struct_depth = 0 return null + else + throw e end end var header = new iostream.char_buff diff --git a/imports/ecs_lint.csp b/imports/ecs_lint.csp index 117cdae..2938b93 100644 --- a/imports/ecs_lint.csp +++ b/imports/ecs_lint.csp @@ -49,8 +49,6 @@ struct lint_context var file_name = "" var code_buff = new array var issues = new array - var indent_info = new hash_map - var indent_level = 0 function add_issue(pos, message, severity, rule_name) var iss = new issue @@ -99,19 +97,6 @@ function is_upper_snake_case(name) return true end -function is_comment_or_blank_line(line) - # Check if line is blank or comment-only (first non-space char is #) - for i = 0, i < line.size, ++i - var ch = line[i] - if ch == '#' - return true # comment line - end - if ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n' - return false # has non-space, non-comment content - end - end - return true # blank line -end # ============================================================ # Lint Rules @@ -279,13 +264,6 @@ function rule_unreachable_code(ctx, nodes) end end -function rule_useless_expression(ctx, nodes) - # TODO: Detect expressions whose results are discarded - # Requires deep analysis of the expression chain to distinguish - # between calls with side effects and pure expressions. - # For now, skip this check. -end - # === Source-level rules (operate on code_buff, not AST) === function rule_trailing_whitespace(ctx) @@ -525,7 +503,6 @@ class linter end function visit_stmts(nodes) - this.ctx.indent_level += 1 # Check unreachable code rule_unreachable_code(this.ctx, nodes) # Recurse into children @@ -534,17 +511,14 @@ class linter this.visit_statement(nodes[i].nodes) end end - this.ctx.indent_level -= 1 end function visit_decl_stmts(nodes) - this.ctx.indent_level += 1 for i = 0, i < nodes.size, ++i if typeid nodes[i] == typeid parsergen.syntax_tree && nodes[i].root == "declaration" this.visit_declaration(nodes[i].nodes) end end - this.ctx.indent_level -= 1 end function visit_statement(nodes) @@ -569,16 +543,10 @@ class linter function visit_function_stmt(nodes) rule_function_naming(this.ctx, nodes) - # Check empty body rule_empty_block(this.ctx, nodes) - # Recurse into body for i = 0, i < nodes.size, ++i if typeid nodes[i] == typeid parsergen.syntax_tree - if nodes[i].root == "stmts" || nodes[i].root == "function-body" - this.visit_node(nodes[i].nodes) - else - this.visit_node(nodes[i].nodes) - end + this.visit_node(nodes[i].nodes) end end end @@ -588,11 +556,7 @@ class linter rule_empty_block(this.ctx, nodes) for i = 0, i < nodes.size, ++i if typeid nodes[i] == typeid parsergen.syntax_tree - if nodes[i].root == "stmts" || nodes[i].root == "function-body" - this.visit_node(nodes[i].nodes) - else - this.visit_node(nodes[i].nodes) - end + this.visit_node(nodes[i].nodes) end end end @@ -659,6 +623,7 @@ class linter end function visit_switch_stmt(nodes) + rule_empty_block(this.ctx, nodes) this.visit_node(nodes) end @@ -690,6 +655,7 @@ class linter end function visit_block_stmt(nodes) + rule_empty_block(this.ctx, nodes) for i = 0, i < nodes.size, ++i if typeid nodes[i] == typeid parsergen.syntax_tree if nodes[i].root == "stmts" @@ -719,7 +685,6 @@ class linter end function visit_expr_stmt(nodes) - rule_useless_expression(this.ctx, nodes) this.visit_node(nodes) end end From 043f7004b0612c779f0c8cf58cf1aa1d1c17196c Mon Sep 17 00:00:00 2001 From: Mike Lee Date: Thu, 23 Jul 2026 11:21:09 +0800 Subject: [PATCH 08/13] feat: enhance linting rules and add tests for indentation consistency and unused expressions --- imports/ecs_bootstrap.csp | 10 ++++-- imports/ecs_format.csp | 7 ++-- imports/ecs_lint.csp | 62 ++++++++++++++++++++-------------- unit_tests/test_lint_rules.ecs | 2 ++ 4 files changed, 52 insertions(+), 29 deletions(-) diff --git a/imports/ecs_bootstrap.csp b/imports/ecs_bootstrap.csp index 9745a3b..bb03add 100644 --- a/imports/ecs_bootstrap.csp +++ b/imports/ecs_bootstrap.csp @@ -229,8 +229,6 @@ function process_args(cmd_args) no_run = true end case "-n" - format_only = true - no_run = true format_dry_run = true end case "-s" @@ -412,6 +410,14 @@ function run_ecs(cmd_args) if exit_code != -1 return exit_code end + # -n implies format + dry-run; lint/format modes skip the compile cache + if format_dry_run + format_only = true + end + if lint_only || format_only + no_run = true + no_hash = true + end if repl var instance = new repl_instance instance.silent = silent diff --git a/imports/ecs_format.csp b/imports/ecs_format.csp index d9b34d4..28dd3a5 100644 --- a/imports/ecs_format.csp +++ b/imports/ecs_format.csp @@ -119,7 +119,9 @@ class format_writer end function pop_indent() - --indent_level + if indent_level > 0 + --indent_level + end end function token(tok) @@ -635,7 +637,7 @@ class format_visitor function visit_for_body(nodes) var idx = 0 if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "do" - ++idx; this.w.write("do") + ++idx; this.w.keyword("do") this.w.space() this.visit_children(nodes[idx++].nodes) this.visit_endline(nodes[idx++].nodes) @@ -817,6 +819,7 @@ class format_visitor var idx = 0 if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "{" ++idx; this.w.write("{") + this.w.space() this.visit_stmts(nodes[idx++].nodes) ++idx; this.w.write("}") end diff --git a/imports/ecs_lint.csp b/imports/ecs_lint.csp index 2938b93..54343d2 100644 --- a/imports/ecs_lint.csp +++ b/imports/ecs_lint.csp @@ -118,19 +118,19 @@ function rule_function_naming(ctx, nodes) end function rule_async_function_naming(ctx, nodes) - # nodes: async-function-stmt children - # Pattern: "async" "function" id "(" ... ")" ... var idx = 0 - while idx < nodes.size - if typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].type == "id" - var name = nodes[idx].data - if !is_snake_case(name) - ctx.add_issue(nodes[idx].pos, "Function name '" + name + "' should be snake_case.", SEVERITY_WARNING, "function_naming") - end - break - end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "async" + ++idx + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "function" ++idx end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].type == "id" + var name = nodes[idx].data + if !is_snake_case(name) + ctx.add_issue(nodes[idx].pos, "Function name '" + name + "' should be snake_case.", SEVERITY_WARNING, "function_naming") + end + end end function rule_variable_naming(ctx, nodes) @@ -211,8 +211,28 @@ function rule_class_naming(ctx, nodes) end end +function check_empty_stmts(ctx, node, first_pos, keyword_name) + if node.root == "stmts" || node.root == "decl-stmts" + var has_stmt = false + for j = 0, j < node.nodes.size, ++j + if typeid node.nodes[j] == typeid parsergen.syntax_tree && node.nodes[j].root == "statement" + has_stmt = true + break + end + end + if !has_stmt + ctx.add_issue(first_pos, "Empty " + keyword_name + " block body.", SEVERITY_WARNING, "empty_block") + end + else + for j = 0, j < node.nodes.size, ++j + if typeid node.nodes[j] == typeid parsergen.syntax_tree + check_empty_stmts(ctx, node.nodes[j], first_pos, keyword_name) + end + end + end +end + function rule_empty_block(ctx, nodes) - # Extract the first meaningful token's position and data var first_pos = {0, 0} var keyword_name = "block" for i = 0, i < nodes.size, ++i @@ -224,19 +244,8 @@ function rule_empty_block(ctx, nodes) keyword_name = nodes[i].data end end - if typeid nodes[i] == typeid parsergen.syntax_tree && nodes[i].root == "stmts" - var stmts_nodes = nodes[i].nodes - var has_stmt = false - for j = 0, j < stmts_nodes.size, ++j - if typeid stmts_nodes[j] == typeid parsergen.syntax_tree && stmts_nodes[j].root == "statement" - has_stmt = true - break - end - end - if !has_stmt - ctx.add_issue(first_pos, "Empty " + keyword_name + " block body.", SEVERITY_WARNING, "empty_block") - end - break + if typeid nodes[i] == typeid parsergen.syntax_tree + check_empty_stmts(ctx, nodes[i], first_pos, keyword_name) end end end @@ -304,6 +313,9 @@ function rule_consecutive_blank_lines(ctx) blank_count = 0 end end + if blank_count > 2 + ctx.add_issue({1, ctx.code_buff.size - blank_count + 2}, "Too many consecutive blank lines (" + to_string(blank_count) + ").", SEVERITY_INFO, "consecutive_blank_lines") + end end function rule_indentation_consistency(ctx) @@ -324,7 +336,7 @@ function rule_indentation_consistency(ctx) has_spaces = true end end - if ch != '\r' && ch != '\n' + if ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n' in_indent = false end end diff --git a/unit_tests/test_lint_rules.ecs b/unit_tests/test_lint_rules.ecs index 0e3b321..b063457 100644 --- a/unit_tests/test_lint_rules.ecs +++ b/unit_tests/test_lint_rules.ecs @@ -111,6 +111,8 @@ assert_ge(unreachable_code_count, 1, "detects unreachable_code errors") assert_ge(empty_block_count, 1, "detects empty_block warnings") assert_ge(trailing_whitespace_count, 1, "detects trailing_whitespace warnings") assert_ge(consecutive_blank_lines_count, 1, "detects consecutive_blank_lines info") +assert_eq(indentation_count, 0, "no indentation_consistency issues in test file") +assert_eq(useless_expression_count, 0, "useless_expression rule not yet implemented") assert_eq(lint.count_by_severity(ecs_lint.SEVERITY_ERROR), error_count, "count_by_severity(error) matches") assert_eq(lint.count_by_severity(ecs_lint.SEVERITY_WARNING), warning_count, "count_by_severity(warning) matches") From 8426f754832d708fe86852b05449dcc35f5e0247 Mon Sep 17 00:00:00 2001 From: Mike Lee Date: Thu, 23 Jul 2026 13:15:12 +0800 Subject: [PATCH 09/13] feat: enhance linter and formatter by improving empty statement checks and syntax handling --- imports/ecs_bootstrap.csp | 31 +++++++++++++++++++------------ imports/ecs_format.csp | 29 +++++++---------------------- imports/ecs_lint.csp | 22 ++++++++++++++++++---- imports/ecs_parser.csp | 10 +++++----- 4 files changed, 49 insertions(+), 43 deletions(-) diff --git a/imports/ecs_bootstrap.csp b/imports/ecs_bootstrap.csp index bb03add..164129e 100644 --- a/imports/ecs_bootstrap.csp +++ b/imports/ecs_bootstrap.csp @@ -236,8 +236,6 @@ function process_args(cmd_args) end case "-r" repl = true - ++index - break end case "-g" no_hash = true @@ -271,7 +269,11 @@ function process_args(cmd_args) exit_code = 1 return end - csx_path = cmd_args[++index] + if csx_path == null + csx_path = cmd_args[++index] + else + csx_path = csx_path + system.path.delimiter + cmd_args[++index] + end end case "-o" if index == cmd_args.size - 1 @@ -450,6 +452,9 @@ function run_ecs(cmd_args) if name != null && !name.empty() var mtime = ifs.getline().to_number() if mtime == system.file.mtime(file_name) + if csx_path != null + compiler_args += " -i " + csx_path + end return no_run ? 0 : system.run(process_path(executor + compiler_args + " " + name + arguments)) end end @@ -459,13 +464,6 @@ function run_ecs(cmd_args) end end end - var parser = new parsergen.generator - if unicode != null - parser.unicode_cvt = setup_unicode(unicode) - if exit_code != -1 - return exit_code - end - end # Lint-only mode (lint_file does its own parsing) if lint_only var result = ecs_lint.lint_file(file_name) @@ -476,6 +474,13 @@ function run_ecs(cmd_args) end return 0 end + var parser = new parsergen.generator + if unicode != null + parser.unicode_cvt = setup_unicode(unicode) + if exit_code != -1 + return exit_code + end + end # Format mode (uses format-specific grammar with com tokens) if format_only var cvt = null @@ -530,7 +535,9 @@ function run_ecs(cmd_args) foreach it in parser.code_buff do csym_ofs.println(it) end if !no_hash - iostream.ofstream("./.ecs_output/" + file_hash).println(target_name) + var ofs = iostream.ofstream("./.ecs_output/" + file_hash) + ofs.println(target_name) + ofs.println(system.file.mtime(file_name)) end if splash != null system.out.println(splash) @@ -561,7 +568,7 @@ function run_ecs(cmd_args) return no_run ? 0 : system.run(process_path(executor + compiler_args + " " + target_name + arguments)) end end - return 0 + return 1 end function main(cmd_args) diff --git a/imports/ecs_format.csp b/imports/ecs_format.csp index 28dd3a5..0611025 100644 --- a/imports/ecs_format.csp +++ b/imports/ecs_format.csp @@ -337,8 +337,13 @@ class format_visitor # Unary operators: no space between operator and operand if !matched && root == "unary-op" matched = true - this.w.write(node.nodes[0].data) - this.w.need_space = false + var op = node.nodes[0].data + this.w.write(op) + if op == "typeid" || op == "not" + this.w.need_space = true + else + this.w.need_space = false + end end # Lambda body: { stmts } or -> expr if !matched && root == "lambda-body" @@ -508,7 +513,6 @@ class format_visitor function visit_if_stmt(nodes) var idx = 0 ++idx; this.w.keyword("if") - this.w.space() this.visit_children(nodes[idx++].nodes) # eol (was endl token) if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" @@ -531,7 +535,6 @@ class format_visitor if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "if" this.w.space() ++idx; this.w.keyword("if") - this.w.space() this.visit_children(nodes[idx++].nodes) end if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" @@ -542,7 +545,6 @@ class format_visitor function visit_while_stmt(nodes) var idx = 0 ++idx; this.w.keyword("while") - this.w.space() this.visit_children(nodes[idx++].nodes) if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" this.visit_eol(nodes[idx++].nodes) @@ -575,7 +577,6 @@ class format_visitor function visit_until_stmt(nodes) var idx = 0 ++idx; this.w.keyword("until") - this.w.space() this.visit_children(nodes[idx++].nodes) if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" this.visit_eol(nodes[idx++].nodes) @@ -585,7 +586,6 @@ class format_visitor function visit_for_stmt(nodes) var idx = 0 ++idx; this.w.keyword("for") - this.w.space() if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "var-def" this.visit_children(nodes[idx++].nodes) end @@ -617,14 +617,12 @@ class format_visitor function visit_foreach_stmt(nodes) var idx = 0 ++idx; this.w.keyword("foreach") - this.w.space() if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].type == "id" this.w.write(nodes[idx++].data) this.w.space() end if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "in" ++idx; this.w.keyword("in") - this.w.space() end if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "basic-expr" this.visit_children(nodes[idx++].nodes) @@ -638,7 +636,6 @@ class format_visitor var idx = 0 if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "do" ++idx; this.w.keyword("do") - this.w.space() this.visit_children(nodes[idx++].nodes) this.visit_endline(nodes[idx++].nodes) end @@ -655,7 +652,6 @@ class format_visitor function visit_switch_stmt(nodes) var idx = 0 ++idx; this.w.keyword("switch") - this.w.space() this.visit_children(nodes[idx++].nodes) if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" this.visit_eol(nodes[idx++].nodes) @@ -689,7 +685,6 @@ class format_visitor function visit_switch_case(nodes) var idx = 0 ++idx; this.w.keyword("case") - this.w.space() this.visit_children(nodes[idx++].nodes) if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" this.visit_eol(nodes[idx++].nodes) @@ -734,7 +729,6 @@ class format_visitor function visit_catch_stmt(nodes) var idx = 0 ++idx; this.w.keyword("catch") - this.w.space() this.w.write(nodes[idx++].data) if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == ":" this.w.write(nodes[idx++].data) @@ -765,10 +759,8 @@ class format_visitor var idx = 0 if is_async ++idx; this.w.keyword("async") - this.w.space() end ++idx; this.w.keyword("function") - this.w.space() this.w.write(nodes[idx++].data) ++idx; this.w.write("(") # skip nl, handle argument-list, skip nl @@ -835,12 +827,10 @@ class format_visitor if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type this.w.keyword(nodes[idx++].data) end - this.w.space() this.w.write(nodes[idx++].data) if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "extends" this.w.space() ++idx; this.w.keyword("extends") - this.w.space() this.visit_children(nodes[idx++].nodes) end if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" @@ -856,7 +846,6 @@ class format_visitor function visit_namespace_stmt(nodes) var idx = 0 ++idx; this.w.keyword("namespace") - this.w.space() this.w.write(nodes[idx++].data) if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "eol" this.visit_eol(nodes[idx++].nodes) @@ -873,7 +862,6 @@ class format_visitor if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type this.w.keyword(nodes[idx++].data) end - this.w.space() this.visit_children(nodes[idx++].nodes) this.visit_endline(nodes[idx++].nodes) end @@ -927,7 +915,6 @@ class format_visitor function visit_import_stmt(nodes) var idx = 0 ++idx; this.w.keyword("import") - this.w.space() this.visit_children(nodes[idx++].nodes) this.visit_endline(nodes[idx++].nodes) end @@ -935,7 +922,6 @@ class format_visitor function visit_package_stmt(nodes) var idx = 0 ++idx; this.w.keyword("package") - this.w.space() this.w.write(nodes[idx++].data) this.visit_endline(nodes[idx++].nodes) end @@ -943,7 +929,6 @@ class format_visitor function visit_using_stmt(nodes) var idx = 0 ++idx; this.w.keyword("using") - this.w.space() this.visit_children(nodes[idx++].nodes) this.visit_endline(nodes[idx++].nodes) end diff --git a/imports/ecs_lint.csp b/imports/ecs_lint.csp index 54343d2..c45552b 100644 --- a/imports/ecs_lint.csp +++ b/imports/ecs_lint.csp @@ -212,7 +212,7 @@ function rule_class_naming(ctx, nodes) end function check_empty_stmts(ctx, node, first_pos, keyword_name) - if node.root == "stmts" || node.root == "decl-stmts" + if node.root == "stmts" var has_stmt = false for j = 0, j < node.nodes.size, ++j if typeid node.nodes[j] == typeid parsergen.syntax_tree && node.nodes[j].root == "statement" @@ -224,9 +224,22 @@ function check_empty_stmts(ctx, node, first_pos, keyword_name) ctx.add_issue(first_pos, "Empty " + keyword_name + " block body.", SEVERITY_WARNING, "empty_block") end else - for j = 0, j < node.nodes.size, ++j - if typeid node.nodes[j] == typeid parsergen.syntax_tree - check_empty_stmts(ctx, node.nodes[j], first_pos, keyword_name) + if node.root == "decl-stmts" + var has_decl = false + for j = 0, j < node.nodes.size, ++j + if typeid node.nodes[j] == typeid parsergen.syntax_tree && node.nodes[j].root == "declaration" + has_decl = true + break + end + end + if !has_decl + ctx.add_issue(first_pos, "Empty " + keyword_name + " block body.", SEVERITY_WARNING, "empty_block") + end + else + for j = 0, j < node.nodes.size, ++j + if typeid node.nodes[j] == typeid parsergen.syntax_tree + check_empty_stmts(ctx, node.nodes[j], first_pos, keyword_name) + end end end end @@ -575,6 +588,7 @@ class linter function visit_class_stmt(nodes) rule_class_naming(this.ctx, nodes) + rule_empty_block(this.ctx, nodes) for i = 0, i < nodes.size, ++i if typeid nodes[i] == typeid parsergen.syntax_tree if nodes[i].root == "decl-stmts" diff --git a/imports/ecs_parser.csp b/imports/ecs_parser.csp index 6f9ba59..d217980 100644 --- a/imports/ecs_parser.csp +++ b/imports/ecs_parser.csp @@ -323,15 +323,15 @@ function get_syntax(strict) syntax.ref("mul-expr"), syntax.optional(syntax.cond_or({syntax.term("+")}, {syntax.term("-")}), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("add-expr")) }, "mul-expr" : { - syntax.ref("conv-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.term("*")}, {syntax.term("/")}, {syntax.term("%")}, {syntax.term("^")}), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("mul-expr")) + syntax.ref("conv-expr"), syntax.optional(syntax.nlook(syntax_eol_fn()), syntax.cond_or({syntax.term("*")}, {syntax.term("/")}, {syntax.term("%")}, {syntax.term("^")}), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("mul-expr")) }, "conv-expr" : { - syntax.ref("unary-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.term("=>")}, {syntax.term("as")}), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("visit-expr")) + syntax.ref("unary-expr"), syntax.optional(syntax.nlook(syntax_eol_fn()), syntax.cond_or({syntax.term("=>")}, {syntax.term("as")}), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("visit-expr")) }, "unary-expr" : {syntax.cond_or( {syntax.ref("unary-op"), syntax.ref("unary-expr")}, {syntax.cond_or({syntax.term("new")}, {syntax.term("gcnew")}), syntax.ref("visit-expr"), syntax.optional(syntax.ref("array"))}, - {syntax.ref("prim-expr"), syntax.optional(syntax.nlook(syntax.token("endl")), syntax.ref("postfix-expr"))} + {syntax.ref("prim-expr"), syntax.optional(syntax.nlook(syntax_eol_fn()), syntax.ref("postfix-expr"))} )}, "unary-op" : {syntax.cond_or( {syntax.term("typeid")}, @@ -367,11 +367,11 @@ function get_syntax(strict) {syntax.token("char")} )}, "ecsx-extend" : { - syntax.token("id"), syntax.nlook(syntax.token("endl")), syntax.term("::"), syntax.token("id"), syntax.term("("), (syntax_op_fn(syntax.ref("nl")))..., syntax.optional(syntax.ref("basic-expr")), (syntax_op_fn(syntax.ref("nl")))..., syntax.term(")") + syntax.token("id"), syntax.nlook(syntax_eol_fn()), syntax.term("::"), syntax.token("id"), syntax.term("("), (syntax_op_fn(syntax.ref("nl")))..., syntax.optional(syntax.ref("basic-expr")), (syntax_op_fn(syntax.ref("nl")))..., syntax.term(")") }, "element" : { syntax.cond_or({syntax.token("id")}, {syntax.term("("), (syntax_op_fn(syntax.ref("nl")))..., syntax.ref("basic-expr"), (syntax_op_fn(syntax.ref("nl")))..., syntax.term(")")}), - syntax.repeat(syntax.nlook(syntax.token("endl")), syntax.cond_or({syntax.ref("fcall")}, {syntax.ref("index")})) + syntax.repeat(syntax.nlook(syntax_eol_fn()), syntax.cond_or({syntax.ref("fcall")}, {syntax.ref("index")})) }, "constant" : {syntax.cond_or( {syntax.token("num")}, From 706f113891d8e548319b456f81ee693b92f40da8 Mon Sep 17 00:00:00 2001 From: Mike Lee Date: Thu, 23 Jul 2026 13:26:39 +0800 Subject: [PATCH 10/13] feat: update version numbers in package metadata and script headers to reflect latest releases --- csbuild/ecs.json | 2 +- csbuild/ecs_bootstrap.json | 2 +- csbuild/ecs_generator.json | 2 +- csbuild/ecs_parser.json | 2 +- imports/ecs.csp | 2 +- imports/ecs_bootstrap.csp | 4 ++-- imports/ecs_generator.csp | 2 +- imports/ecs_parser.csp | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/csbuild/ecs.json b/csbuild/ecs.json index 020ac97..3e88aad 100644 --- a/csbuild/ecs.json +++ b/csbuild/ecs.json @@ -3,7 +3,7 @@ "Name": "ecs", "Info": "Extended CovScript(ECS Lang) Header", "Author": "Michael Lee", - "Version": "1.4.1", + "Version": "1.4.2", "Target": "imports/ecs.csp", "Dependencies": [ "sdk_extension" diff --git a/csbuild/ecs_bootstrap.json b/csbuild/ecs_bootstrap.json index bd71798..25730d1 100644 --- a/csbuild/ecs_bootstrap.json +++ b/csbuild/ecs_bootstrap.json @@ -3,7 +3,7 @@ "Name": "ecs_bootstrap", "Info": "Extended CovScript(ECS Lang) Bootstrap", "Author": "Michael Lee", - "Version": "1.7.0", + "Version": "1.7.1", "Target": "imports/ecs_bootstrap.csp", "Dependencies": [ "parsergen", diff --git a/csbuild/ecs_generator.json b/csbuild/ecs_generator.json index c85c443..14be71d 100644 --- a/csbuild/ecs_generator.json +++ b/csbuild/ecs_generator.json @@ -3,7 +3,7 @@ "Name": "ecs_generator", "Info": "Extended CovScript(ECS Lang) Generator", "Author": "Michael Lee", - "Version": "4.1.0.2", + "Version": "4.1.0.3", "Target": "imports/ecs_generator.csp", "Dependencies": [ "parsergen", diff --git a/csbuild/ecs_parser.json b/csbuild/ecs_parser.json index 6679c7d..49069f3 100644 --- a/csbuild/ecs_parser.json +++ b/csbuild/ecs_parser.json @@ -3,7 +3,7 @@ "Name": "ecs_parser", "Info": "Extended CovScript(ECS Lang) Parser", "Author": "Michael Lee", - "Version": "1.3.6", + "Version": "1.4.0", "Target": "imports/ecs_parser.csp", "Dependencies": [ "parsergen", diff --git a/imports/ecs.csp b/imports/ecs.csp index 3c7e76b..dd2d8b4 100644 --- a/imports/ecs.csp +++ b/imports/ecs.csp @@ -1,4 +1,4 @@ -# Extended Covariant Script Header: v1.4.0 +# Extended Covariant Script Header: v1.4.2 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/imports/ecs_bootstrap.csp b/imports/ecs_bootstrap.csp index 164129e..a3783e4 100644 --- a/imports/ecs_bootstrap.csp +++ b/imports/ecs_bootstrap.csp @@ -1,4 +1,4 @@ -# Bootstrap of Extended Covariant Script Generator v1.7.0 +# Bootstrap of Extended Covariant Script Generator v1.7.1 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ package ecs_bootstrap import parsergen, ecs_parser, ecs_generator, ecs_lint, ecs_format, codec, regex import sdk_extension as sdk -var wrapper_ver = "1.7.0" +var wrapper_ver = "1.7.1" var exit_code = -1 # -1: continue, 0: success, >0: error function show_version_simple() diff --git a/imports/ecs_generator.csp b/imports/ecs_generator.csp index 5c2a3c8..6a1458e 100644 --- a/imports/ecs_generator.csp +++ b/imports/ecs_generator.csp @@ -40,7 +40,7 @@ package ecs_generator # Master namespace ecs_info - constant version = "4.1.0 Cuon alpinus(Stable) Build 2" + constant version = "4.1.0 Cuon alpinus(Stable) Build 3" constant std_version = "260702" constant min_runtime = "260702" end diff --git a/imports/ecs_parser.csp b/imports/ecs_parser.csp index d217980..9cffb1a 100644 --- a/imports/ecs_parser.csp +++ b/imports/ecs_parser.csp @@ -1,4 +1,4 @@ -# Covariant Script Parser Generator: Grammar of Extended CovScript(ECS Lang) v1.3.5 +# Covariant Script Parser Generator: Grammar of Extended CovScript(ECS Lang) v1.4.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 3721e7280af98f4da41a9ebfa8b00b34ec561aa5 Mon Sep 17 00:00:00 2001 From: Mike Lee Date: Fri, 24 Jul 2026 00:39:33 +0800 Subject: [PATCH 11/13] feat: enhance linter and formatter with new rules for variable naming and useless expressions --- imports/ecs_bootstrap.csp | 27 +- imports/ecs_format.csp | 526 ++++++++++++++++++++++++++++++--- imports/ecs_lint.csp | 82 ++++- unit_tests/test_lint_rules.ecs | 6 +- 4 files changed, 593 insertions(+), 48 deletions(-) diff --git a/imports/ecs_bootstrap.csp b/imports/ecs_bootstrap.csp index a3783e4..c4ddaaa 100644 --- a/imports/ecs_bootstrap.csp +++ b/imports/ecs_bootstrap.csp @@ -155,9 +155,9 @@ function show_help() " -f Disable compile cache\n" + " -m Disable beautify\n" + " -c Check grammar only\n" + - " -l Lint check\n" + - " -F Format source file\n" + - " -n Dry-run format (print to stdout)\n" + + " -l Lint check (requires file argument)\n" + + " -F Format source file in-place\n" + + " -n Dry-run format to stdout (implies -F)\n" + " -g Generate cSYM info\n" + " -d Run debugger\n" + " -o Set output path\n" + @@ -191,6 +191,7 @@ var silent = false var splash = null var output = null var csym = false +var explicit_csym = false var repl = false var lint_only = false var format_only = false @@ -240,6 +241,7 @@ function process_args(cmd_args) case "-g" no_hash = true csym = true + explicit_csym = true end case "-d" executor = "cs_dbg -s " @@ -402,6 +404,7 @@ function run_ecs(cmd_args) splash = null output = null csym = false + explicit_csym = false repl = false lint_only = false format_only = false @@ -416,9 +419,27 @@ function run_ecs(cmd_args) if format_dry_run format_only = true end + if lint_only && format_only + system.out.println("Warning: -l and -F are mutually exclusive; lint mode takes precedence.") + format_only = false + format_dry_run = false + end if lint_only || format_only no_run = true no_hash = true + if output != null + system.out.println("Warning: -o is ignored in lint/format mode.") + end + if executor != "cs " + system.out.println("Warning: -d is ignored in lint/format mode.") + end + if explicit_csym + system.out.println("Warning: -g is ignored in lint/format mode.") + end + end + if repl && (lint_only || format_only) + system.out.println("Error: -l/-F is incompatible with REPL mode.") + return 1 end if repl var instance = new repl_instance diff --git a/imports/ecs_format.csp b/imports/ecs_format.csp index 0611025..b0dbf9b 100644 --- a/imports/ecs_format.csp +++ b/imports/ecs_format.csp @@ -38,45 +38,31 @@ class format_writer var output = "" var indent_size = 4 var indent_level = 0 - var at_line_start = true var need_space = false - var continuation = 0 var last_text = "" var force_space = false + var max_width = 120 + var continuation = 0 + + var line_buf = new array + var break_points = new array + var line_width = 0 - # Returns true if text starts like an identifier (letter / underscore / CJK) function is_ident(text) if text.empty() return false end var c = to_integer(text[0]) - # A-Z, a-z, _, CJK Unified Ideographs, Extension A, Compatibility return (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c == 95 || (c >= 13312 && c <= 40959) || (c >= 63744 && c <= 64255) end function write(text) - if at_line_start - # Closing brackets align to base indent, not continuation - if text == "}" || text == ")" || text == "]" - continuation = 0 - end - for i = 0, i < (indent_level + continuation) * indent_size, ++i - output += ' ' - end - at_line_start = false - need_space = false - force_space = false - continuation = 0 - last_text = "" - end if need_space || force_space var suppress = false if !force_space - # Never space before punctuation / closers if text == "," || text == ";" || text == "." || text == "->" || text == "::" || text == ")" || text == "]" || text == "}" suppress = true end - # Space before ( [ only in non-call context (after operators / keywords) if !suppress && (text == "(" || text == "[") if last_text == ")" || last_text == "]" suppress = true @@ -88,14 +74,15 @@ class format_writer end end if !suppress - output += ' ' + line_buf.push_back(" ") + line_width += 1 end need_space = false force_space = false end - output += text + line_buf.push_back(text) + line_width += text.size last_text = text - # Operators / keywords set need_space; openers / accessors don't if text == "(" || text == "[" || text == "{" || text == "." || text == "->" || text == "::" || text == "!" need_space = false else @@ -103,11 +90,115 @@ class format_writer end end + function mark_break() + if !line_buf.empty() + break_points.push_back(line_buf.size - 1) + end + end + function newline() + this.flush_line() output += '\n' - at_line_start = true need_space = false force_space = false + last_text = "" + end + + function flush_line() + if line_buf.empty() + return + end + var indent_str = "" + for i = 0, i < (indent_level + continuation) * indent_size, ++i + indent_str += ' ' + end + continuation = 0 + var cont_indent_str = "" + for i = 0, i < (indent_level + 1) * indent_size, ++i + cont_indent_str += ' ' + end + var available = max_width - indent_str.size + if available < 0 + available = 0 + end + + var buf = line_buf + var bps = break_points + line_buf = new array + break_points = new array + line_width = 0 + + output += indent_str + + var first = true + while !buf.empty() + var w = 0 + for i = 0, i < buf.size, ++i + w += buf[i].size + end + + if w <= available + for i = 0, i < buf.size, ++i + output += buf[i] + end + break + end + + var bp = -1 + for i = bps.size - 1, i >= 0, --i + var bw = 0 + for j = 0, j <= bps[i], ++j + bw += buf[j].size + end + if bw <= available + bp = bps[i] + break + end + end + + if bp < 0 + for i = 0, i < buf.size, ++i + output += buf[i] + end + break + end + + for j = 0, j <= bp, ++j + output += buf[j] + end + + var rest = new array + var start = bp + 1 + while start < buf.size && buf[start] == " " + ++start + end + for j = start, j < buf.size, ++j + rest.push_back(buf[j]) + end + + if rest.empty() + break + end + + output += '\n' + if first + available = max_width - cont_indent_str.size + if available < 0 + available = 0 + end + end + output += cont_indent_str + + first = false + buf = rest + var new_bps = new array + for i = 0, i < bps.size, ++i + if bps[i] > bp + new_bps.push_back(bps[i] - bp - 1) + end + end + bps = new_bps + end end function space() @@ -143,10 +234,12 @@ end class format_visitor var w = null + var skip_next_eos = false function run(file_name, code_buff, ast) this.w = new format_writer this.visit_begin(ast.nodes) + this.w.flush_line() return this.w.output end @@ -157,12 +250,14 @@ class format_visitor var node = nodes[idx] if typeid node == typeid parsergen.token_type if node.type == "com" - # Standalone comment inside expression (edge case) this.w.write(node.data) this.w.newline() else if node.type != "endl" this.w.token(node) + if node.data == "," || node.data == "+" || node.data == "-" || node.data == "*" || node.data == "/" || node.data == "%" || node.data == "^" || node.data == "=" || node.data == "+=" || node.data == "-=" || node.data == "*=" || node.data == "/=" || node.data == "%=" || node.data == "==" || node.data == "!=" || node.data == "<" || node.data == ">" || node.data == "<=" || node.data == ">=" || node.data == "&&" || node.data == "||" + this.w.mark_break() + end end end end @@ -334,6 +429,42 @@ class format_visitor matched = true this.visit_nl(node.nodes) end + if !matched && root == "array" + matched = true + this.visit_array(node.nodes) + end + if !matched && root == "fcall" + matched = true + this.visit_fcall(node.nodes) + end + if !matched && root == "lambda-expr" + matched = true + this.visit_lambda_expr(node.nodes) + end + if !matched && root == "async-lambda-expr" + matched = true + this.visit_async_lambda_expr(node.nodes) + end + if !matched && root == "var-bind" + matched = true + this.visit_var_bind(node.nodes) + end + if !matched && root == "bind-expr" + matched = true + this.visit_bind_expr(node.nodes) + end + if !matched && root == "cond-postfix" + matched = true + this.visit_cond_postfix(node.nodes) + end + if !matched && root == "element" + matched = true + this.visit_element(node.nodes) + end + if !matched && root == "ecsx-extend" + matched = true + this.visit_ecsx_extend(node.nodes) + end # Unary operators: no space between operator and operand if !matched && root == "unary-op" matched = true @@ -345,6 +476,12 @@ class format_visitor this.w.need_space = false end end + # Postfix operators: no space before ++/--/... + if !matched && root == "postfix-expr" + matched = true + this.w.need_space = false + this.visit_children(node.nodes) + end # Lambda body: { stmts } or -> expr if !matched && root == "lambda-body" matched = true @@ -388,10 +525,9 @@ class format_visitor end function visit_nl(nodes) - # nl = repeat(endl) — newlines within multi-line expressions if nodes.size > 0 - this.w.continuation = 1 this.w.newline() + this.w.continuation = 1 end end @@ -401,7 +537,6 @@ class format_visitor if nodes.empty() return end - # stmts is the first/only child of begin for idx = 0, idx < nodes[0].nodes.size, ++idx var node = nodes[0].nodes[idx] if typeid node == typeid parsergen.syntax_tree @@ -409,7 +544,15 @@ class format_visitor this.visit_statement(node.nodes) end if node.root == "eos" - this.visit_eos(node.nodes) + if this.skip_next_eos && node.nodes.size == 1 + this.skip_next_eos = false + else + this.skip_next_eos = false + this.visit_eos(node.nodes) + end + end + if node.root == "eol" + this.visit_eol(node.nodes) end end if typeid node == typeid parsergen.token_type @@ -430,7 +573,12 @@ class format_visitor this.visit_statement(node.nodes) end if node.root == "eos" - this.visit_eos(node.nodes) + if this.skip_next_eos && node.nodes.size == 1 + this.skip_next_eos = false + else + this.skip_next_eos = false + this.visit_eos(node.nodes) + end end if node.root == "eol" this.visit_eol(node.nodes) @@ -455,7 +603,12 @@ class format_visitor this.visit_declaration(node.nodes) end if node.root == "eos" - this.visit_eos(node.nodes) + if this.skip_next_eos && node.nodes.size == 1 + this.skip_next_eos = false + else + this.skip_next_eos = false + this.visit_eos(node.nodes) + end end if node.root == "eol" this.visit_eol(node.nodes) @@ -491,16 +644,127 @@ class format_visitor end end + function visit_array(nodes) + var idx = 0 + if idx >= nodes.size + return + end + if typeid nodes[idx] == typeid parsergen.token_type + this.w.write(nodes[idx++].data) + end + var multiline = false + for i = idx, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree && nodes[i].root == "nl" && nodes[i].nodes.size > 0 + multiline = true + break + end + end + if multiline + this.w.newline() + this.w.push_indent() + while idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" + ++idx + end + end + while idx < nodes.size + var node = nodes[idx] + if typeid node == typeid parsergen.token_type + if node.data == "}" || node.data == "]" + break + end + this.w.token(node) + ++idx + else + if typeid node == typeid parsergen.syntax_tree + if node.root == "nl" + if multiline + this.w.newline() + end + ++idx + else + this.dispatch(node) + this.w.continuation = 0 + ++idx + end + end + end + end + if multiline + while idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" + ++idx + end + this.w.pop_indent() + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type + this.w.write(nodes[idx++].data) + end + end + + function visit_fcall(nodes) + var idx = 0 + if idx >= nodes.size + return + end + if typeid nodes[idx] == typeid parsergen.token_type + this.w.write(nodes[idx++].data) + end + var multiline = false + for i = idx, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree && nodes[i].root == "nl" && nodes[i].nodes.size > 0 + multiline = true + break + end + end + if multiline + this.w.newline() + this.w.push_indent() + while idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" + ++idx + end + end + while idx < nodes.size + var node = nodes[idx] + if typeid node == typeid parsergen.token_type + if node.data == ")" + break + end + this.w.token(node) + ++idx + else + if typeid node == typeid parsergen.syntax_tree + if node.root == "nl" + if multiline + this.w.newline() + end + ++idx + else + this.dispatch(node) + ++idx + end + end + end + end + if multiline + while idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" + ++idx + end + this.w.pop_indent() + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type + this.w.write(nodes[idx++].data) + end + end + function visit_endline(nodes) - # endline = eol | ";" - # Delegate to dispatch for eol handling for idx = 0, idx < nodes.size, ++idx var node = nodes[idx] if typeid node == typeid parsergen.syntax_tree + this.skip_next_eos = false this.dispatch(node) end if typeid node == typeid parsergen.token_type if node.data == ";" + this.skip_next_eos = true this.w.write(";") this.w.newline() end @@ -753,6 +1017,181 @@ class format_visitor end end + function visit_lambda_expr_impl(nodes, is_async) + var idx = 0 + if is_async + ++idx; this.w.keyword("async") + end + ++idx; this.w.write("[") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "capture-list" + this.visit_children(nodes[idx++].nodes) + end + ++idx; this.w.write("]") + ++idx; this.w.write("(") + var arg_multiline = false + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" + if nodes[idx].nodes.size > 0 + arg_multiline = true + end + ++idx + end + if arg_multiline + this.w.newline() + this.w.push_indent() + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "argument-list" + this.visit_children(nodes[idx++].nodes) + end + if arg_multiline + this.w.pop_indent() + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" + if arg_multiline + this.w.newline() + end + ++idx + end + ++idx; this.w.write(")") + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "lambda-body" + this.visit_lambda_body(nodes[idx++].nodes) + end + end + + function visit_lambda_expr(nodes) + this.visit_lambda_expr_impl(nodes, false) + end + + function visit_async_lambda_expr(nodes) + this.visit_lambda_expr_impl(nodes, true) + end + + function visit_parenthesized(nodes) + var idx = 0 + if idx >= nodes.size + return + end + if typeid nodes[idx] == typeid parsergen.token_type + this.w.write(nodes[idx++].data) + end + var multiline = false + for i = idx, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree && nodes[i].root == "nl" && nodes[i].nodes.size > 0 + multiline = true + break + end + end + if multiline + this.w.newline() + this.w.push_indent() + while idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" + ++idx + end + end + while idx < nodes.size + var node = nodes[idx] + if typeid node == typeid parsergen.token_type + if node.data == ")" + break + end + if node.data == "," + this.w.write(",") + this.w.mark_break() + ++idx + else + this.w.token(node) + ++idx + end + else + if typeid node == typeid parsergen.syntax_tree + if node.root == "nl" + if multiline + this.w.newline() + end + ++idx + else + this.dispatch(node) + ++idx + end + end + end + end + if multiline + while idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" + ++idx + end + this.w.pop_indent() + end + if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type + this.w.write(nodes[idx++].data) + end + end + + function visit_var_bind(nodes) + this.visit_parenthesized(nodes) + end + + function visit_bind_expr(nodes) + this.visit_parenthesized(nodes) + end + + function visit_cond_postfix(nodes) + var idx = 0 + if idx >= nodes.size + return + end + var multiline = false + for i = 0, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree && nodes[i].root == "nl" && nodes[i].nodes.size > 0 + multiline = true + break + end + end + var pushed = false + while idx < nodes.size + var node = nodes[idx] + if typeid node == typeid parsergen.token_type + if node.data == "?" || node.data == ":" + if multiline && pushed + this.w.pop_indent() + this.w.newline() + pushed = false + end + this.w.write(node.data) + this.w.mark_break() + if multiline + this.w.newline() + this.w.push_indent() + pushed = true + end + ++idx + end + else + if typeid node == typeid parsergen.syntax_tree + if node.root == "nl" + if multiline + this.w.newline() + end + ++idx + else + this.dispatch(node) + ++idx + end + end + end + end + if pushed + this.w.pop_indent() + end + end + + function visit_element(nodes) + this.visit_children(nodes) + end + + function visit_ecsx_extend(nodes) + this.visit_children(nodes) + end + # === Declarations === function visit_function_stmt_impl(nodes, is_async) @@ -763,14 +1202,27 @@ class format_visitor ++idx; this.w.keyword("function") this.w.write(nodes[idx++].data) ++idx; this.w.write("(") - # skip nl, handle argument-list, skip nl + var arg_multiline = false if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" + if nodes[idx].nodes.size > 0 + arg_multiline = true + end ++idx end + if arg_multiline + this.w.newline() + this.w.push_indent() + end if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "argument-list" this.visit_children(nodes[idx++].nodes) end + if arg_multiline + this.w.pop_indent() + end if idx < nodes.size && typeid nodes[idx] == typeid parsergen.syntax_tree && nodes[idx].root == "nl" + if arg_multiline + this.w.newline() + end ++idx end ++idx; this.w.write(")") @@ -793,7 +1245,8 @@ class format_visitor var idx = 0 if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "{" ++idx; this.w.write("{") - this.w.space() + this.w.newline() + this.skip_next_eos = true this.visit_stmts(nodes[idx++].nodes) ++idx; this.w.write("}") end @@ -811,7 +1264,8 @@ class format_visitor var idx = 0 if idx < nodes.size && typeid nodes[idx] == typeid parsergen.token_type && nodes[idx].data == "{" ++idx; this.w.write("{") - this.w.space() + this.w.newline() + this.skip_next_eos = true this.visit_stmts(nodes[idx++].nodes) ++idx; this.w.write("}") end diff --git a/imports/ecs_lint.csp b/imports/ecs_lint.csp index c45552b..3f6283b 100644 --- a/imports/ecs_lint.csp +++ b/imports/ecs_lint.csp @@ -20,7 +20,7 @@ package ecs_lint -import parsergen, ecs_parser, regex +import parsergen, ecs_parser # ============================================================ # Severity Levels @@ -157,10 +157,9 @@ end function check_var_names(ctx, def_nodes, is_constant) var didx = 0 - # var-def is either var-bind = expr OR var-list if didx < def_nodes.size && typeid def_nodes[didx] == typeid parsergen.syntax_tree if def_nodes[didx].root == "var-bind" - # Skip bind patterns for now — too complex to check naming + check_var_bind(ctx, def_nodes[didx].nodes, is_constant) return end if def_nodes[didx].root == "var-list" @@ -169,6 +168,26 @@ function check_var_names(ctx, def_nodes, is_constant) end end +function check_var_bind(ctx, nodes, is_constant) + for i = 0, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.token_type && nodes[i].type == "id" + var name = nodes[i].data + if is_constant + if !is_upper_snake_case(name) + ctx.add_issue(nodes[i].pos, "Constant name '" + name + "' should be UPPER_SNAKE_CASE.", SEVERITY_WARNING, "constant_naming") + end + else + if !is_snake_case(name) + ctx.add_issue(nodes[i].pos, "Variable name '" + name + "' should be snake_case.", SEVERITY_WARNING, "variable_naming") + end + end + end + if typeid nodes[i] == typeid parsergen.syntax_tree + check_var_bind(ctx, nodes[i].nodes, is_constant) + end + end +end + function check_var_list(ctx, nodes, is_constant) var idx = 0 while idx < nodes.size @@ -253,7 +272,7 @@ function rule_empty_block(ctx, nodes) if first_pos[1] == 0 first_pos = nodes[i].pos end - if keyword_name == "block" && nodes[i].data != "endl" + if keyword_name == "block" && nodes[i].data != "endl" && nodes[i].data != "async" keyword_name = nodes[i].data end end @@ -286,6 +305,56 @@ function rule_unreachable_code(ctx, nodes) end end +function is_side_effect_free(nodes) + for i = 0, i < nodes.size, ++i + if typeid nodes[i] == typeid parsergen.syntax_tree + var root = nodes[i].root + if root == "fcall" || root == "index" + return false + end + if root == "asi-op" + return false + end + if root == "unary-op" + if !nodes[i].nodes.empty() + var op = nodes[i].nodes[0].data + if op == "++" || op == "--" + return false + end + end + end + if root == "postfix-expr" + if !nodes[i].nodes.empty() + var pf = nodes[i].nodes[0].data + if pf == "++" || pf == "--" + return false + end + end + end + if !is_side_effect_free(nodes[i].nodes) + return false + end + end + if typeid nodes[i] == typeid parsergen.token_type + var data = nodes[i].data + if data == "new" || data == "gcnew" + return false + end + end + end + return true +end + +function rule_useless_expression(ctx, nodes) + if nodes.empty() + return + end + if is_side_effect_free(nodes) + var pos = get_pos(nodes) + ctx.add_issue(pos, "Useless expression with no side effect.", SEVERITY_WARNING, "useless_expression") + end +end + # === Source-level rules (operate on code_buff, not AST) === function rule_trailing_whitespace(ctx) @@ -321,13 +390,13 @@ function rule_consecutive_blank_lines(ctx) ++blank_count else if blank_count > 2 - ctx.add_issue({1, i - blank_count + 2}, "Too many consecutive blank lines (" + to_string(blank_count) + ").", SEVERITY_INFO, "consecutive_blank_lines") + ctx.add_issue({1, i - blank_count}, "Too many consecutive blank lines (" + to_string(blank_count) + ").", SEVERITY_INFO, "consecutive_blank_lines") end blank_count = 0 end end if blank_count > 2 - ctx.add_issue({1, ctx.code_buff.size - blank_count + 2}, "Too many consecutive blank lines (" + to_string(blank_count) + ").", SEVERITY_INFO, "consecutive_blank_lines") + ctx.add_issue({1, ctx.code_buff.size - blank_count}, "Too many consecutive blank lines (" + to_string(blank_count) + ").", SEVERITY_INFO, "consecutive_blank_lines") end end @@ -711,6 +780,7 @@ class linter end function visit_expr_stmt(nodes) + rule_useless_expression(this.ctx, nodes) this.visit_node(nodes) end end diff --git a/unit_tests/test_lint_rules.ecs b/unit_tests/test_lint_rules.ecs index b063457..ad51f72 100644 --- a/unit_tests/test_lint_rules.ecs +++ b/unit_tests/test_lint_rules.ecs @@ -112,15 +112,15 @@ assert_ge(empty_block_count, 1, "detects empty_block warnings") assert_ge(trailing_whitespace_count, 1, "detects trailing_whitespace warnings") assert_ge(consecutive_blank_lines_count, 1, "detects consecutive_blank_lines info") assert_eq(indentation_count, 0, "no indentation_consistency issues in test file") -assert_eq(useless_expression_count, 0, "useless_expression rule not yet implemented") +assert_ge(useless_expression_count, 1, "detects useless_expression warnings") assert_eq(lint.count_by_severity(ecs_lint.SEVERITY_ERROR), error_count, "count_by_severity(error) matches") assert_eq(lint.count_by_severity(ecs_lint.SEVERITY_WARNING), warning_count, "count_by_severity(warning) matches") assert_eq(lint.count_by_severity(ecs_lint.SEVERITY_INFO), info_count, "count_by_severity(info) matches") assert_ge(error_count, 1, "has at least one severity_error") -assert_ge(warning_count, 5, "has at least five severity_warning") -assert_ge(issues.size, 6, "total issues >= 6") +assert_ge(warning_count, 6, "has at least six severity_warning") +assert_ge(issues.size, 7, "total issues >= 7") system.out.println("") system.out.println("Test: lint rules -- " + to_string(passed) + " passed, " + to_string(failed) + " failed") From 47409ae56366cda489333684aabcff4ab2b23cd1 Mon Sep 17 00:00:00 2001 From: Mike Lee Date: Fri, 24 Jul 2026 10:10:41 +0800 Subject: [PATCH 12/13] feat: enhance error handling in run_ecs function to return appropriate exit codes based on linting results --- imports/ecs_bootstrap.csp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/imports/ecs_bootstrap.csp b/imports/ecs_bootstrap.csp index c4ddaaa..c8ac43e 100644 --- a/imports/ecs_bootstrap.csp +++ b/imports/ecs_bootstrap.csp @@ -490,6 +490,14 @@ function run_ecs(cmd_args) var result = ecs_lint.lint_file(file_name) if result != null result[0].print_report() + if result[0].count_by_severity(ecs_lint.SEVERITY_ERROR) > 0 + return 1 + end + else + return 1 + end + return 0 + end else return 1 end From 49011a423e8e7dbb33a3fb99d5525498ae927ebf Mon Sep 17 00:00:00 2001 From: Mike Lee Date: Fri, 24 Jul 2026 10:20:48 +0800 Subject: [PATCH 13/13] feat: simplify run_ecs function by removing redundant return statements --- imports/ecs_bootstrap.csp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/imports/ecs_bootstrap.csp b/imports/ecs_bootstrap.csp index c8ac43e..97c68ff 100644 --- a/imports/ecs_bootstrap.csp +++ b/imports/ecs_bootstrap.csp @@ -497,11 +497,6 @@ function run_ecs(cmd_args) return 1 end return 0 - end - else - return 1 - end - return 0 end var parser = new parsergen.generator if unicode != null