graphql_tag_pluck 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fcbca0ff3be0816489e30ce795f1be443b956469afb4510b00cf8ccdf1a8245e
4
- data.tar.gz: d51b601e49320c4a13e79ba371d2e169f8d7b93f231db025fe2e2fa0d89f7203
3
+ metadata.gz: ba2b33d369224dd7521d95c6cd1cae8839bf350694d10343872e4e98e153d5dd
4
+ data.tar.gz: 3f5a2db373cff91ceb6916be998f6ad3082dee1f4cc1124505e6db68ace3ff73
5
5
  SHA512:
6
- metadata.gz: 50fe1a5bc7e76a7b542bfb30ef080e3eb4a9c64aa5eb6bddce88d53814cbe7a94ea4b1221dea60ba0a2cd3f183e91721eb343503ae6367fee8aa635945d44e5d
7
- data.tar.gz: 6c449f2a1901ce559208e27b1ef99e696ffab4209f7d55d2f763a23da84fb7d47fe1c027b56c72b30c7821da335917baa142973cc73432f5dce71ebad047c644
6
+ metadata.gz: a93e45d44f8b3f9f89449447962ae3380fe4b0bcdd34528bf58c5b62b8a0f0e840d4eb423bc2e41032c6a0e5c047e571e31740f3928f888f8bb043477f1e467a
7
+ data.tar.gz: 171fccfe0c3775df5cd4917fea80567ad41c14509c79036ec56c0e8361348e772165d23af5ac994a9171d5f79e799424546bd565cd61b110cb13f3e8375b0181
data/CHANGELOG.md CHANGED
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-11-03
11
+
12
+ ### Added
13
+
14
+ - Each exported entry now records where it was defined: `file` (relative to
15
+ the current directory, or absolute for files outside it), `line` (the line
16
+ the definition starts on, also correct for several definitions in one
17
+ heredoc), and `constant` (the fully qualified constant the heredoc is
18
+ assigned to, such as `Clients::UserClient::USER_QUERY`, or `null`).
19
+ ([#16](https://github.com/indigolain/graphql_tag_pluck/pull/16))
20
+
21
+ ### Changed
22
+
23
+ - **Breaking:** unnamed operations are keyed by their location, such as
24
+ `"app/clients/user_client.rb:4"`, instead of `unnamed_operation_N`. The old
25
+ counter depended on the order files were read in.
26
+ ([#16](https://github.com/indigolain/graphql_tag_pluck/pull/16))
27
+ - **Breaking:** any definition name that appears more than once, even with an
28
+ identical definition, now raises `GraphqlTagPluck::Error` naming both
29
+ locations, and no output file is written. 0.2.0 only warned and kept the
30
+ last definition.
31
+ ([#16](https://github.com/indigolain/graphql_tag_pluck/pull/16))
32
+ - Files are processed in sorted order, so output no longer depends on file
33
+ system order. Warnings for invalid GraphQL now include the `path:line`.
34
+ ([#16](https://github.com/indigolain/graphql_tag_pluck/pull/16))
35
+
10
36
  ## [0.2.0] - 2026-11-03
11
37
 
12
38
  ### Changed
@@ -65,7 +91,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
65
91
  - Configuration through `.graphqltagpluckconfig.yaml`
66
92
  (`graphql_heredoc_identifiers`, `file_glob_pattern`, `output_path`).
67
93
 
68
- [Unreleased]: https://github.com/indigolain/graphql_tag_pluck/compare/v0.2.0...HEAD
94
+ [Unreleased]: https://github.com/indigolain/graphql_tag_pluck/compare/v0.3.0...HEAD
95
+ [0.3.0]: https://github.com/indigolain/graphql_tag_pluck/compare/v0.2.0...v0.3.0
69
96
  [0.2.0]: https://github.com/indigolain/graphql_tag_pluck/compare/v0.1.1...v0.2.0
70
97
  [0.1.1]: https://github.com/indigolain/graphql_tag_pluck/compare/v0.1.0...v0.1.1
71
98
  [0.1.0]: https://github.com/indigolain/graphql_tag_pluck/releases/tag/v0.1.0
data/README.md CHANGED
@@ -32,11 +32,26 @@ will output json file like: (following json content is formatted)
32
32
  "SampleQuery": {
33
33
  "name": "SampleQuery",
34
34
  "source": "query SampleQuery {\n hoge\n fuga {\n piyo\n }\n}",
35
- "type": "query"
35
+ "type": "query",
36
+ "file": "app/clients/sample_client.rb",
37
+ "line": 2,
38
+ "constant": "SAMPLE_QUERY"
36
39
  }
37
40
  }
38
41
  ```
39
42
 
43
+ Each entry contains:
44
+
45
+ - `name`: the operation / fragment name (`null` for unnamed operations)
46
+ - `source`: the normalized GraphQL document of the definition
47
+ - `type`: `query`, `mutation`, `subscription`, or `fragment`
48
+ - `file`: the file the definition was found in, relative to the current directory
49
+ - `line`: the line the definition starts on
50
+ - `constant`: the fully qualified constant the heredoc is assigned to (e.g. `Clients::UserClient::USER_QUERY`), or `null` when the heredoc is not assigned to a constant
51
+
52
+ Entries are keyed by definition name. Unnamed operations are keyed by their location (e.g. `"app/clients/sample_client.rb:2"`).
53
+ Definition names must be unique: any name defined more than once, even identically, raises `GraphqlTagPluck::Error` and no file is written.
54
+
40
55
  by executing
41
56
 
42
57
  ```sh
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.3.0
@@ -5,8 +5,6 @@ module GraphqlTagPluck
5
5
  class Error < StandardError; end
6
6
 
7
7
  class << self
8
- using GraphqlTagPluck::Parser::AST::NodeExt
9
-
10
8
  attr_accessor :options
11
9
 
12
10
  def default_options
@@ -18,9 +16,9 @@ module GraphqlTagPluck
18
16
  end
19
17
 
20
18
  def get_graphql_heredocs
21
- files = Dir.glob(GraphqlTagPluck.options[:file_glob_pattern])
22
- unnamed_operation_count = 0
23
- origin_files = {}
19
+ # Sorted so the output, and which duplicate wins, does not depend on file system order.
20
+ files = Dir.glob(GraphqlTagPluck.options[:file_glob_pattern]).sort
21
+ collector = HeredocCollector.new
24
22
 
25
23
  {}.tap do |hash|
26
24
  files.each do |file|
@@ -30,40 +28,53 @@ module GraphqlTagPluck
30
28
  warn "graphql_tag_pluck: skipping #{file}, which could not be parsed as Ruby: #{e.message}"
31
29
  next
32
30
  end
33
- parsed_node_array = node&.parse_node_recursively
34
- next if parsed_node_array.nil?
31
+ next if node.nil?
32
+
33
+ path = relative_path(file)
35
34
 
36
- parsed_node_array.flatten.compact.each do |graphql_doc_string|
37
- parsed_graphql_doc_string = begin
38
- GraphQL.parse(graphql_doc_string)
35
+ collector.collect(node).each do |heredoc|
36
+ document = begin
37
+ GraphQL.parse(heredoc.source)
39
38
  rescue GraphQL::ParseError => e
40
- warn "graphql_tag_pluck: skipping invalid GraphQL heredoc in #{file}: #{e.message}"
39
+ warn "graphql_tag_pluck: skipping invalid GraphQL heredoc in #{path}:#{heredoc.line}: #{e.message}"
41
40
  next
42
41
  end
43
- parsed_graphql_doc_string.definitions.each do |definition|
44
- key = if definition.name.nil?
45
- unnamed_operation_count += 1
46
- "unnamed_operation_#{unnamed_operation_count}"
47
- else
48
- definition.name
49
- end
50
42
 
43
+ document.definitions.each do |definition|
44
+ line = heredoc.line + definition.line - 1
45
+ # Unnamed operations are keyed by their location so keys stay stable across runs.
46
+ key = definition.name || "#{path}:#{line}"
51
47
  source = definition.to_query_string
52
- if hash.key?(key) && hash[key][:source] != source
53
- warn "graphql_tag_pluck: #{key} in #{file} overwrites a different definition with the same name in #{origin_files[key]}"
48
+
49
+ # Operation names must be unique, even for identical copies, so the
50
+ # generated list never silently depends on which definition wins.
51
+ if (existing = hash[key])
52
+ raise Error, "Duplicate GraphQL definition name `#{key}`: " \
53
+ "defined at #{existing[:file]}:#{existing[:line]} and #{path}:#{line}"
54
54
  end
55
- origin_files[key] = file
56
55
 
57
56
  hash[key] = {
58
57
  name: definition.name,
59
58
  source: source,
60
- type: definition.instance_of?(GraphQL::Language::Nodes::FragmentDefinition) ? 'fragment' : definition.operation_type
59
+ type: definition.instance_of?(GraphQL::Language::Nodes::FragmentDefinition) ? 'fragment' : definition.operation_type,
60
+ file: path,
61
+ line: line,
62
+ constant: heredoc.constant
61
63
  }
62
64
  end
63
65
  end
64
66
  end
65
67
  end
66
68
  end
69
+
70
+ private
71
+
72
+ # Relative to the current directory when the file is inside it, absolute otherwise.
73
+ def relative_path(file)
74
+ absolute = File.realpath(file)
75
+ prefix = "#{File.realpath(Dir.pwd)}/"
76
+ absolute.start_with?(prefix) ? absolute.delete_prefix(prefix) : absolute
77
+ end
67
78
  end
68
79
 
69
80
  self.options = default_options.merge(Config.load)
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlTagPluck
4
+ # Walks a Ruby AST and collects GraphQL heredocs together with
5
+ # the line their body starts on and the constant they are assigned to.
6
+ class HeredocCollector
7
+ using GraphqlTagPluck::Parser::AST::NodeExt
8
+
9
+ Heredoc = Struct.new(:source, :line, :constant, keyword_init: true)
10
+
11
+ def collect(node)
12
+ [].tap { |result| visit(node, [], nil, result) }
13
+ end
14
+
15
+ private
16
+
17
+ # namespace: lexical module / class nesting, e.g. ["Clients", "UserClient"]
18
+ # constant: fully qualified constant name the current node is assigned to, if any
19
+ def visit(node, namespace, constant, result)
20
+ return unless node.is_a?(::Parser::AST::Node)
21
+
22
+ if node.is_graphql_heredoc_node?
23
+ if node.interpolated?
24
+ warn "graphql_tag_pluck: skipping GraphQL heredoc with Ruby interpolation at #{node.loc.expression.source_buffer.name}:#{node.loc.line}"
25
+ else
26
+ body = node.loc.heredoc_body
27
+ result << Heredoc.new(source: body.source, line: body.line, constant: constant)
28
+ end
29
+ return
30
+ end
31
+
32
+ case node.type
33
+ when :module, :class
34
+ name_node, *rest = node.children
35
+ inner_namespace = nest(namespace, const_name(name_node))
36
+ rest.each { |child| visit(child, inner_namespace, nil, result) }
37
+ when :casgn
38
+ scope, name, value = node.children
39
+ visit(value, namespace, assigned_constant(namespace, scope, name), result)
40
+ when :send, :csend
41
+ # Keep the constant for receivers so `FOO = <<~GRAPHQL.freeze` is still attributed to FOO,
42
+ # but not for arguments: `FOO = wrap(<<~GRAPHQL)` does not assign the heredoc itself.
43
+ receiver, _method_name, *args = node.children
44
+ visit(receiver, namespace, constant, result)
45
+ args.each { |child| visit(child, namespace, nil, result) }
46
+ else
47
+ node.children.each { |child| visit(child, namespace, nil, result) }
48
+ end
49
+ end
50
+
51
+ # Returns e.g. "Foo::Bar", "::Foo", or nil when the constant path is dynamic (e.g. `self::Foo`).
52
+ def const_name(node)
53
+ return nil unless node.is_a?(::Parser::AST::Node) && node.type == :const
54
+
55
+ scope, name = node.children
56
+ case scope&.type
57
+ when nil then name.to_s
58
+ when :cbase then "::#{name}"
59
+ when :const
60
+ prefix = const_name(scope)
61
+ prefix && "#{prefix}::#{name}"
62
+ end
63
+ end
64
+
65
+ def nest(namespace, name)
66
+ return namespace if name.nil?
67
+ return [name.delete_prefix("::")] if name.start_with?("::")
68
+
69
+ namespace + [name]
70
+ end
71
+
72
+ def assigned_constant(namespace, scope, name)
73
+ return (namespace + [name.to_s]).join("::") if scope.nil?
74
+ return name.to_s if scope.type == :cbase
75
+
76
+ prefix = const_name(scope)
77
+ prefix && nest(nest(namespace, prefix), name.to_s).join("::")
78
+ end
79
+ end
80
+ end
@@ -9,19 +9,6 @@ module GraphqlTagPluck
9
9
  type == :dstr && children.any? { |child| child.type != :str && !(child.type == :dstr && !child.interpolated?) }
10
10
  end
11
11
 
12
- def parse_node_recursively
13
- return nil unless respond_to?(:loc)
14
- if is_graphql_heredoc_node?
15
- return loc.heredoc_body.source unless interpolated?
16
-
17
- warn "graphql_tag_pluck: skipping GraphQL heredoc with Ruby interpolation at #{loc.expression.source_buffer.name}:#{loc.line}"
18
- return nil
19
- end
20
- if respond_to?(:children)
21
- children.map {|child| child.respond_to?(:parse_node_recursively) ? child.parse_node_recursively : nil }
22
- end
23
- end
24
-
25
12
  def is_graphql_heredoc_node?
26
13
  loc.instance_of?(::Parser::Source::Map::Heredoc) &&
27
14
  GraphqlTagPluck.options[:graphql_heredoc_identifiers].include?(heredoc_identifier)
@@ -7,6 +7,7 @@ require "yaml"
7
7
  require_relative "graphql_tag_pluck/version"
8
8
  require_relative "graphql_tag_pluck/config"
9
9
  require_relative "graphql_tag_pluck/parser/ast/node_ext"
10
+ require_relative "graphql_tag_pluck/heredoc_collector"
10
11
  require_relative "graphql_tag_pluck/railtie" if defined?(Rails)
11
12
 
12
13
  require_relative 'graphql_tag_pluck/graphql_tag_pluck'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql_tag_pluck
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - indigolain
@@ -91,6 +91,7 @@ files:
91
91
  - lib/graphql_tag_pluck/Rakefile
92
92
  - lib/graphql_tag_pluck/config.rb
93
93
  - lib/graphql_tag_pluck/graphql_tag_pluck.rb
94
+ - lib/graphql_tag_pluck/heredoc_collector.rb
94
95
  - lib/graphql_tag_pluck/parser/ast/node_ext.rb
95
96
  - lib/graphql_tag_pluck/railtie.rb
96
97
  - lib/graphql_tag_pluck/rake_tasks.rb