diff --git a/etc/completion.bash b/etc/completion.bash index d8731c65bff7..9263289b5b4e 100644 --- a/etc/completion.bash +++ b/etc/completion.bash @@ -66,7 +66,7 @@ _crystal() _crystal_compgen_options "${opts}" "${cur}" else if [[ "${prev}" == "tool" ]] ; then - local subcommands="context dependencies format hierarchy implementations types" + local subcommands="context dependencies flags format hierarchy implementations types" _crystal_compgen_options "${subcommands}" "${cur}" else _crystal_compgen_sources "${cur}" diff --git a/etc/completion.fish b/etc/completion.fish index 150dd37108d8..64fc6a97b45a 100644 --- a/etc/completion.fish +++ b/etc/completion.fish @@ -1,5 +1,5 @@ set -l crystal_commands init build clear_cache docs env eval i interactive play run spec tool help version -set -l tool_subcommands context expand format hierarchy implementations types +set -l tool_subcommands context expand flags format hierarchy implementations types complete -c crystal -s h -l help -d "Show help" -x @@ -173,6 +173,8 @@ complete -c crystal -n "__fish_seen_subcommand_from expand" -s p -l progress -d complete -c crystal -n "__fish_seen_subcommand_from expand" -s t -l time -d "Enable execution time output" complete -c crystal -n "__fish_seen_subcommand_from expand" -l stdin-filename -d "Source file name to be read from STDIN" +complete -c crystal -n "__fish_seen_subcommand_from tool; and not __fish_seen_subcommand_from $tool_subcommands" -a "flags" -d "print all macro 'flag?' values" -x + complete -c crystal -n "__fish_seen_subcommand_from tool; and not __fish_seen_subcommand_from $tool_subcommands" -a "format" -d "format project, directories and/or files" -x complete -c crystal -n "__fish_seen_subcommand_from format" -l check -d "Checks that formatting code produces no changes" complete -c crystal -n "__fish_seen_subcommand_from format" -s i -l include -d "Include path" diff --git a/etc/completion.zsh b/etc/completion.zsh index bf57d80208df..ffa12798ca18 100644 --- a/etc/completion.zsh +++ b/etc/completion.zsh @@ -165,6 +165,7 @@ _crystal-tool() { "context:show context for given location" "dependencies:show tree of required source files" "expand:show macro expansion for given location" + "flags:print all macro 'flag?' values" "format:format project, directories and/or files" "hierarchy:show type hierarchy" "implementations:show implementations for given call in location" @@ -211,6 +212,12 @@ _crystal-tool() { $cursor_args ;; + (flags) + _arguments \ + $programfile \ + $help_args + ;; + (format) _arguments \ $programfile \ diff --git a/man/crystal.1 b/man/crystal.1 index 3a2a3d4a1b2a..e137959dd5ad 100644 --- a/man/crystal.1 +++ b/man/crystal.1 @@ -367,7 +367,7 @@ Disable colored output. .Op -- .Op arguments .Pp -Run a tool. The available tools are: context, dependencies, format, hierarchy, implementations, and types. +Run a tool. The available tools are: context, dependencies, flags, format, hierarchy, implementations, and types. .Pp Tools: .Bl -tag -offset indent @@ -396,6 +396,8 @@ Show skipped and heads of filtered paths .El .It Cm expand Show macro expansion for given location. +.It Cm flags +Print all macro 'flag?' values .It Cm format Format project, directories and/or files with the coding style used in the standard library. You can use the .Fl -check diff --git a/spec/compiler/crystal/tools/flags_spec.cr b/spec/compiler/crystal/tools/flags_spec.cr new file mode 100644 index 000000000000..c295439e0547 --- /dev/null +++ b/spec/compiler/crystal/tools/flags_spec.cr @@ -0,0 +1,44 @@ +require "../../../spec_helper" +include Crystal + +private def parse_flags(source) + Crystal::Command::FlagsVisitor.new.tap do |visitor| + Parser.parse(source).accept(visitor) + end +end + +describe Crystal::Command::FlagsVisitor do + it "different flags" do + visitor = parse_flags <<-CRYSTAL + {% + flag?(:foo) + flag?("bar") + flag?(1) + flag?(true) + %} + CRYSTAL + visitor.flag_names.should eq %w[1 bar foo true] + end + + it "unique flags" do + visitor = parse_flags <<-CRYSTAL + {% + flag?(:foo) + flag?("foo") + flag?(:foo) + %} + CRYSTAL + visitor.flag_names.should eq %w[foo] + end + + it "only macro" do + visitor = parse_flags <<-CRYSTAL + flag?(:flag) + f.flag?(:foo) + F.flag?(:bar) + {% f.flag?(:baz) %} + {% f.flag?(:qux, other: true) %} + CRYSTAL + visitor.flag_names.should eq %w[] + end +end diff --git a/src/compiler/crystal/command.cr b/src/compiler/crystal/command.cr index 97bf46feb663..651bc62900ef 100644 --- a/src/compiler/crystal/command.cr +++ b/src/compiler/crystal/command.cr @@ -41,6 +41,7 @@ class Crystal::Command Tool: context show context for given location expand show macro expansion for given location + flags print all macro `flag?` values format format project, directories and/or files hierarchy show type hierarchy dependencies show file dependency tree @@ -177,6 +178,9 @@ class Crystal::Command when "format".starts_with?(tool) options.shift format + when "flags" == tool + options.shift + flags when "expand".starts_with?(tool) options.shift expand diff --git a/src/compiler/crystal/tools/flags.cr b/src/compiler/crystal/tools/flags.cr new file mode 100644 index 000000000000..17bb15007021 --- /dev/null +++ b/src/compiler/crystal/tools/flags.cr @@ -0,0 +1,104 @@ +require "colorize" +require "../syntax/ast" + +class Crystal::Command + private def flags + OptionParser.parse(@options) do |opts| + opts.banner = "Usage: crystal tool flags [path...]\n\nOptions:" + + opts.on("-h", "--help", "Show this message") do + puts opts + exit + end + + opts.on("--no-color", "Disable colored output") do + @color = false + end + end + + visitor = FlagsVisitor.new + find_sources(options) do |source| + Parser.parse(source.code).accept(visitor) + end + visitor.flag_names.each do |flag| + puts flag + end + end + + def find_sources( + paths : Array(String), + stdin : IO = STDIN, + & : Compiler::Source -> + ) : Nil + stdin_content = nil + paths.each do |path| + if path == "-" + stdin_content ||= stdin.gets_to_end + yield Compiler::Source.new(path, stdin_content) + elsif File.file?(path) + yield Compiler::Source.new(path, File.read(path)) + elsif Dir.exists?(path) + Dir.glob(::Path[path].to_posix.join("**/*.cr")) do |dir_path| + if File.file?(dir_path) + yield Compiler::Source.new(path, File.read(dir_path)) + end + end + else + Crystal.error "file or directory does not exist: #{path}", @color, leading_error: false + end + end + end + + class FlagsVisitor < Visitor + @in_macro_expression = false + + getter all_flags = [] of ASTNode + + def initialize(@flag_name : String = "flag?") + end + + def flag_names + all_flags.map { |flag| string_flag(flag) }.uniq!.sort! + end + + private def string_flag(node) + case node + when StringLiteral, SymbolLiteral + node.value + else + node.to_s + end + end + + def visit(node) + true + end + + def visit(node : Crystal::MacroExpression | Crystal::MacroIf | Crystal::MacroFor) + @in_macro_expression = true + + true + end + + def end_visit(node : Crystal::MacroExpression | Crystal::MacroIf | Crystal::MacroFor) + @in_macro_expression = false + end + + def visit(node : Crystal::Call) + check_call(node) + true + end + + private def check_call(node) + return unless @in_macro_expression + return unless node.name == @flag_name + return unless node.obj.nil? && node.block.nil? && node.named_args.nil? + + args = node.args + return unless args.size == 1 + arg = args[0] + + all_flags << arg + end + end +end