isort 0.2.0 → 0.2.1
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 +4 -4
- data/isort-0.2.0.gem +0 -0
- data/lib/isort/file_processor.rb +426 -0
- data/lib/isort/import_block.rb +164 -0
- data/lib/isort/import_statement.rb +126 -0
- data/lib/isort/parser.rb +173 -0
- data/lib/isort/section.rb +197 -0
- data/lib/isort/syntax_validator.rb +75 -0
- data/lib/isort/version.rb +1 -1
- data/lib/isort/wrap_modes.rb +240 -0
- data/test_local.rb +106 -0
- metadata +11 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "section"
|
|
4
|
+
|
|
5
|
+
module Isort
|
|
6
|
+
# Represents a single import statement with all its metadata
|
|
7
|
+
# This includes the raw line, type, sort key, and associated comments
|
|
8
|
+
class ImportStatement
|
|
9
|
+
IMPORT_TYPES = %i[require require_relative include extend autoload using].freeze
|
|
10
|
+
TYPE_ORDER = {
|
|
11
|
+
require: 0,
|
|
12
|
+
require_relative: 1,
|
|
13
|
+
include: 2,
|
|
14
|
+
extend: 3,
|
|
15
|
+
autoload: 4,
|
|
16
|
+
using: 5
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
SKIP_PATTERN = /#\s*isort:\s*skip\b/i.freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :type, :raw_line, :sort_key, :leading_comments, :indentation
|
|
22
|
+
attr_reader :skip_sorting, :section
|
|
23
|
+
|
|
24
|
+
def initialize(raw_line:, type:, leading_comments: [], indentation: "")
|
|
25
|
+
@raw_line = raw_line
|
|
26
|
+
@type = type
|
|
27
|
+
@leading_comments = leading_comments
|
|
28
|
+
@indentation = indentation
|
|
29
|
+
@sort_key = extract_sort_key
|
|
30
|
+
@skip_sorting = has_skip_directive?
|
|
31
|
+
@section = Section.classify(self)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Check if this import has an isort:skip directive
|
|
35
|
+
def has_skip_directive?
|
|
36
|
+
@raw_line.match?(SKIP_PATTERN)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
alias skip_sorting? skip_sorting
|
|
40
|
+
|
|
41
|
+
# Returns the full representation including leading comments
|
|
42
|
+
def to_s
|
|
43
|
+
lines = []
|
|
44
|
+
lines.concat(@leading_comments) unless @leading_comments.empty?
|
|
45
|
+
lines << @raw_line
|
|
46
|
+
lines.join
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns lines as an array (for reconstruction)
|
|
50
|
+
def to_lines
|
|
51
|
+
result = @leading_comments.dup
|
|
52
|
+
result << @raw_line
|
|
53
|
+
result
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# For deduplication - normalized import path/module
|
|
57
|
+
def normalized_key
|
|
58
|
+
@normalized_key ||= begin
|
|
59
|
+
stripped = @raw_line.strip
|
|
60
|
+
# Extract the actual import value for comparison
|
|
61
|
+
case @type
|
|
62
|
+
when :require, :require_relative
|
|
63
|
+
# Extract path from require 'path' or require "path"
|
|
64
|
+
if (match = stripped.match(/^(?:require|require_relative)\s+['"]([^'"]+)['"]/))
|
|
65
|
+
"#{@type}:#{match[1]}"
|
|
66
|
+
else
|
|
67
|
+
"#{@type}:#{stripped}"
|
|
68
|
+
end
|
|
69
|
+
when :include, :extend, :using
|
|
70
|
+
# Extract module name
|
|
71
|
+
if (match = stripped.match(/^(?:include|extend|using)\s+(\S+)/))
|
|
72
|
+
"#{@type}:#{match[1]}"
|
|
73
|
+
else
|
|
74
|
+
"#{@type}:#{stripped}"
|
|
75
|
+
end
|
|
76
|
+
when :autoload
|
|
77
|
+
# Extract constant and path
|
|
78
|
+
if (match = stripped.match(/^autoload\s+:?(\w+)/))
|
|
79
|
+
"#{@type}:#{match[1]}"
|
|
80
|
+
else
|
|
81
|
+
"#{@type}:#{stripped}"
|
|
82
|
+
end
|
|
83
|
+
else
|
|
84
|
+
"#{@type}:#{stripped}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Compare for sorting - first by section, then by type order, then alphabetically
|
|
90
|
+
def <=>(other)
|
|
91
|
+
# First compare by section (stdlib, thirdparty, firstparty, localfolder)
|
|
92
|
+
section_comparison = Section.order(@section) <=> Section.order(other.section)
|
|
93
|
+
return section_comparison unless section_comparison.zero?
|
|
94
|
+
|
|
95
|
+
# Then by type within section
|
|
96
|
+
type_comparison = TYPE_ORDER[@type] <=> TYPE_ORDER[other.type]
|
|
97
|
+
return type_comparison unless type_comparison.zero?
|
|
98
|
+
|
|
99
|
+
# Finally alphabetically
|
|
100
|
+
@sort_key <=> other.sort_key
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def extract_sort_key
|
|
106
|
+
stripped = @raw_line.strip
|
|
107
|
+
# Remove the keyword and extract the sortable part
|
|
108
|
+
case @type
|
|
109
|
+
when :require
|
|
110
|
+
stripped.sub(/^require\s+/, "")
|
|
111
|
+
when :require_relative
|
|
112
|
+
stripped.sub(/^require_relative\s+/, "")
|
|
113
|
+
when :include
|
|
114
|
+
stripped.sub(/^include\s+/, "")
|
|
115
|
+
when :extend
|
|
116
|
+
stripped.sub(/^extend\s+/, "")
|
|
117
|
+
when :autoload
|
|
118
|
+
stripped.sub(/^autoload\s+/, "")
|
|
119
|
+
when :using
|
|
120
|
+
stripped.sub(/^using\s+/, "")
|
|
121
|
+
else
|
|
122
|
+
stripped
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
data/lib/isort/parser.rb
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Isort
|
|
4
|
+
# Parses Ruby source lines and classifies them by type
|
|
5
|
+
# This is the core component for understanding file structure
|
|
6
|
+
class Parser
|
|
7
|
+
# Line types that can be returned
|
|
8
|
+
LINE_TYPES = %i[
|
|
9
|
+
shebang
|
|
10
|
+
magic_comment
|
|
11
|
+
require
|
|
12
|
+
require_relative
|
|
13
|
+
include
|
|
14
|
+
extend
|
|
15
|
+
autoload
|
|
16
|
+
using
|
|
17
|
+
comment
|
|
18
|
+
blank
|
|
19
|
+
code
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
IMPORT_TYPES = %i[require require_relative include extend autoload using].freeze
|
|
23
|
+
|
|
24
|
+
# Magic comment patterns (encoding, frozen_string_literal, etc.)
|
|
25
|
+
MAGIC_COMMENT_PATTERN = /^#\s*(?:encoding|coding|frozen_string_literal|warn_indent|shareable_constant_value):/i
|
|
26
|
+
|
|
27
|
+
# Skip directive patterns
|
|
28
|
+
SKIP_LINE_PATTERN = /#\s*isort:\s*skip\b/i.freeze
|
|
29
|
+
SKIP_FILE_PATTERN = /^#\s*isort:\s*skip_file\b/i.freeze
|
|
30
|
+
|
|
31
|
+
def initialize
|
|
32
|
+
@line_number = 0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Classify a single line and return its type
|
|
36
|
+
def classify_line(line, line_number: nil)
|
|
37
|
+
@line_number = line_number if line_number
|
|
38
|
+
stripped = line.to_s.strip
|
|
39
|
+
|
|
40
|
+
return :blank if stripped.empty?
|
|
41
|
+
return classify_comment(line, stripped) if stripped.start_with?("#")
|
|
42
|
+
|
|
43
|
+
classify_code(stripped)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Check if a line type is an import
|
|
47
|
+
def import_type?(type)
|
|
48
|
+
IMPORT_TYPES.include?(type)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Extract the indentation from a line
|
|
52
|
+
def extract_indentation(line)
|
|
53
|
+
match = line.to_s.match(/^(\s*)/)
|
|
54
|
+
match ? match[1] : ""
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Check if line is a shebang (must be line 1)
|
|
58
|
+
def shebang?(line, line_number)
|
|
59
|
+
line_number == 1 && line.to_s.strip.start_with?("#!")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check if line has an isort:skip directive
|
|
63
|
+
def has_skip_directive?(line)
|
|
64
|
+
line.to_s.match?(SKIP_LINE_PATTERN) && !line.to_s.match?(SKIP_FILE_PATTERN)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Check if line has an isort:skip_file directive
|
|
68
|
+
def has_skip_file_directive?(line)
|
|
69
|
+
line.to_s.match?(SKIP_FILE_PATTERN)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def classify_comment(line, stripped)
|
|
75
|
+
# Check for shebang on first line
|
|
76
|
+
return :shebang if @line_number == 1 && stripped.start_with?("#!")
|
|
77
|
+
|
|
78
|
+
# Check for magic comments (only valid at top of file, but we classify anyway)
|
|
79
|
+
return :magic_comment if stripped.match?(MAGIC_COMMENT_PATTERN)
|
|
80
|
+
|
|
81
|
+
:comment
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def classify_code(stripped)
|
|
85
|
+
# Skip lines that are primarily string literals containing import keywords
|
|
86
|
+
# e.g., puts "require 'json'" or error_msg = 'include Module'
|
|
87
|
+
return :code if string_containing_import_keyword?(stripped)
|
|
88
|
+
|
|
89
|
+
# Order matters - check more specific patterns first
|
|
90
|
+
|
|
91
|
+
# require_relative must come before require
|
|
92
|
+
return :require_relative if require_relative_line?(stripped)
|
|
93
|
+
return :require if require_line?(stripped)
|
|
94
|
+
return :include if include_line?(stripped)
|
|
95
|
+
return :extend if extend_line?(stripped)
|
|
96
|
+
return :autoload if autoload_line?(stripped)
|
|
97
|
+
return :using if using_line?(stripped)
|
|
98
|
+
|
|
99
|
+
:code
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Detect if a line is primarily a string that happens to contain import keywords
|
|
103
|
+
# This prevents false positives like: puts "require 'json'" or x = "include Foo"
|
|
104
|
+
def string_containing_import_keyword?(stripped)
|
|
105
|
+
# If line starts with a string assignment or method call with string arg
|
|
106
|
+
# and the import keyword appears inside quotes, it's not a real import
|
|
107
|
+
|
|
108
|
+
# Pattern: variable = "...require..." or variable = '...require...'
|
|
109
|
+
return true if stripped.match?(/^\w+\s*=\s*['"].*(?:require|require_relative|include|extend|autoload|using).*['"]/)
|
|
110
|
+
|
|
111
|
+
# Pattern: method_call "...require..." or method_call '...require...'
|
|
112
|
+
# e.g., puts "require 'json'"
|
|
113
|
+
return true if stripped.match?(/^\w+\s+['"].*(?:require|require_relative|include|extend|autoload|using).*['"]/)
|
|
114
|
+
|
|
115
|
+
# Pattern: method_call("...require...") - method with parens and string arg
|
|
116
|
+
return true if stripped.match?(/^\w+\(["'].*(?:require|require_relative|include|extend|autoload|using).*["']\)/)
|
|
117
|
+
|
|
118
|
+
# Check if import keyword appears after an opening quote (inside a string)
|
|
119
|
+
# This catches cases like: desc "require the json gem"
|
|
120
|
+
if stripped.include?('"') || stripped.include?("'")
|
|
121
|
+
# Find position of first quote and import keyword
|
|
122
|
+
first_quote_pos = [stripped.index('"'), stripped.index("'")].compact.min
|
|
123
|
+
import_keywords = %w[require require_relative include extend autoload using]
|
|
124
|
+
|
|
125
|
+
import_keywords.each do |keyword|
|
|
126
|
+
keyword_pos = stripped.index(keyword)
|
|
127
|
+
next unless keyword_pos
|
|
128
|
+
|
|
129
|
+
# If keyword appears after a quote, it might be inside a string
|
|
130
|
+
# But we need to ensure it's not at the start (real import)
|
|
131
|
+
if first_quote_pos && keyword_pos > first_quote_pos && keyword_pos > 0
|
|
132
|
+
return true
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
false
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def require_line?(stripped)
|
|
141
|
+
# Match: require 'foo' or require "foo" or require('foo')
|
|
142
|
+
# But NOT: require_relative
|
|
143
|
+
stripped.match?(/^require\s+['"]/) || stripped.match?(/^require\(['"]/)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def require_relative_line?(stripped)
|
|
147
|
+
# Match: require_relative 'foo' or require_relative "foo"
|
|
148
|
+
stripped.match?(/^require_relative\s+['"]/) || stripped.match?(/^require_relative\(['"]/)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def include_line?(stripped)
|
|
152
|
+
# Match: include ModuleName or include(ModuleName)
|
|
153
|
+
# But NOT: included, includes, include?
|
|
154
|
+
stripped.match?(/^include\s+[A-Z]/) || stripped.match?(/^include\([A-Z]/)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def extend_line?(stripped)
|
|
158
|
+
# Match: extend ModuleName
|
|
159
|
+
# But NOT: extended, extends
|
|
160
|
+
stripped.match?(/^extend\s+[A-Z]/) || stripped.match?(/^extend\([A-Z]/)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def autoload_line?(stripped)
|
|
164
|
+
# Match: autoload :Constant, 'path' or autoload(:Constant, 'path')
|
|
165
|
+
stripped.match?(/^autoload\s+:/) || stripped.match?(/^autoload\(:/)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def using_line?(stripped)
|
|
169
|
+
# Match: using ModuleName
|
|
170
|
+
stripped.match?(/^using\s+[A-Z]/) || stripped.match?(/^using\([A-Z]/)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Isort
|
|
6
|
+
# Categorizes imports into sections based on their source
|
|
7
|
+
# - STDLIB: Ruby standard library
|
|
8
|
+
# - THIRDPARTY: Gems/external packages
|
|
9
|
+
# - FIRSTPARTY: Project's own modules
|
|
10
|
+
# - LOCALFOLDER: Relative imports
|
|
11
|
+
class Section
|
|
12
|
+
SECTIONS = %i[stdlib thirdparty firstparty localfolder].freeze
|
|
13
|
+
|
|
14
|
+
SECTION_ORDER = {
|
|
15
|
+
stdlib: 0,
|
|
16
|
+
thirdparty: 1,
|
|
17
|
+
firstparty: 2,
|
|
18
|
+
localfolder: 3
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# Ruby standard library modules (partial list of common ones)
|
|
22
|
+
# This list covers the most commonly used stdlib modules
|
|
23
|
+
STDLIB_MODULES = Set.new(%w[
|
|
24
|
+
abbrev
|
|
25
|
+
base64
|
|
26
|
+
benchmark
|
|
27
|
+
bigdecimal
|
|
28
|
+
cgi
|
|
29
|
+
csv
|
|
30
|
+
date
|
|
31
|
+
delegate
|
|
32
|
+
digest
|
|
33
|
+
drb
|
|
34
|
+
english
|
|
35
|
+
erb
|
|
36
|
+
etc
|
|
37
|
+
fcntl
|
|
38
|
+
fiddle
|
|
39
|
+
fileutils
|
|
40
|
+
find
|
|
41
|
+
forwardable
|
|
42
|
+
getoptlong
|
|
43
|
+
io/console
|
|
44
|
+
io/nonblock
|
|
45
|
+
io/wait
|
|
46
|
+
ipaddr
|
|
47
|
+
irb
|
|
48
|
+
json
|
|
49
|
+
logger
|
|
50
|
+
matrix
|
|
51
|
+
minitest
|
|
52
|
+
monitor
|
|
53
|
+
mutex_m
|
|
54
|
+
net/ftp
|
|
55
|
+
net/http
|
|
56
|
+
net/https
|
|
57
|
+
net/imap
|
|
58
|
+
net/pop
|
|
59
|
+
net/smtp
|
|
60
|
+
nkf
|
|
61
|
+
objspace
|
|
62
|
+
observer
|
|
63
|
+
open-uri
|
|
64
|
+
open3
|
|
65
|
+
openssl
|
|
66
|
+
optparse
|
|
67
|
+
ostruct
|
|
68
|
+
pathname
|
|
69
|
+
pp
|
|
70
|
+
prettyprint
|
|
71
|
+
prime
|
|
72
|
+
pstore
|
|
73
|
+
psych
|
|
74
|
+
racc
|
|
75
|
+
rake
|
|
76
|
+
rdoc
|
|
77
|
+
readline
|
|
78
|
+
resolv
|
|
79
|
+
ripper
|
|
80
|
+
rss
|
|
81
|
+
securerandom
|
|
82
|
+
set
|
|
83
|
+
shellwords
|
|
84
|
+
singleton
|
|
85
|
+
socket
|
|
86
|
+
stringio
|
|
87
|
+
strscan
|
|
88
|
+
syslog
|
|
89
|
+
tempfile
|
|
90
|
+
time
|
|
91
|
+
timeout
|
|
92
|
+
tmpdir
|
|
93
|
+
tracer
|
|
94
|
+
tsort
|
|
95
|
+
un
|
|
96
|
+
uri
|
|
97
|
+
weakref
|
|
98
|
+
webrick
|
|
99
|
+
yaml
|
|
100
|
+
zlib
|
|
101
|
+
]).freeze
|
|
102
|
+
|
|
103
|
+
class << self
|
|
104
|
+
# Classify an import statement into a section
|
|
105
|
+
def classify(statement)
|
|
106
|
+
case statement.type
|
|
107
|
+
when :require
|
|
108
|
+
classify_require(statement)
|
|
109
|
+
when :require_relative
|
|
110
|
+
:localfolder
|
|
111
|
+
when :include, :extend, :using
|
|
112
|
+
classify_module(statement)
|
|
113
|
+
when :autoload
|
|
114
|
+
:firstparty # autoload is typically project-specific
|
|
115
|
+
else
|
|
116
|
+
:thirdparty
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Get the section order for sorting
|
|
121
|
+
def order(section)
|
|
122
|
+
SECTION_ORDER[section] || 999
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def classify_require(statement)
|
|
128
|
+
# Extract the require path
|
|
129
|
+
stripped = statement.raw_line.strip
|
|
130
|
+
path = nil
|
|
131
|
+
|
|
132
|
+
if (match = stripped.match(/^require\s+['"]([^'"]+)['"]/))
|
|
133
|
+
path = match[1]
|
|
134
|
+
elsif (match = stripped.match(/^require\(['"]([^'"]+)['"]\)/))
|
|
135
|
+
path = match[1]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
return :thirdparty unless path
|
|
139
|
+
|
|
140
|
+
# Check if it's a stdlib module
|
|
141
|
+
if stdlib_module?(path)
|
|
142
|
+
:stdlib
|
|
143
|
+
else
|
|
144
|
+
:thirdparty
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def classify_module(statement)
|
|
149
|
+
# Include, extend, using are typically project-specific
|
|
150
|
+
# unless they're from well-known gems
|
|
151
|
+
stripped = statement.raw_line.strip
|
|
152
|
+
|
|
153
|
+
# Extract module name
|
|
154
|
+
module_name = nil
|
|
155
|
+
if (match = stripped.match(/^(?:include|extend|using)\s+(\S+)/))
|
|
156
|
+
module_name = match[1]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
return :firstparty unless module_name
|
|
160
|
+
|
|
161
|
+
# Check for known stdlib modules
|
|
162
|
+
if stdlib_constant?(module_name)
|
|
163
|
+
:stdlib
|
|
164
|
+
else
|
|
165
|
+
:firstparty
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def stdlib_module?(path)
|
|
170
|
+
# Remove file extension if present
|
|
171
|
+
base_path = path.sub(/\.rb$/, "")
|
|
172
|
+
|
|
173
|
+
# Check direct match
|
|
174
|
+
return true if STDLIB_MODULES.include?(base_path)
|
|
175
|
+
|
|
176
|
+
# Check prefix (e.g., 'net/http' for 'net/http/response')
|
|
177
|
+
STDLIB_MODULES.any? do |mod|
|
|
178
|
+
base_path.start_with?("#{mod}/") || base_path == mod
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def stdlib_constant?(name)
|
|
183
|
+
# Common Ruby stdlib constants
|
|
184
|
+
stdlib_constants = %w[
|
|
185
|
+
Comparable
|
|
186
|
+
Enumerable
|
|
187
|
+
Forwardable
|
|
188
|
+
Observable
|
|
189
|
+
Singleton
|
|
190
|
+
MonitorMixin
|
|
191
|
+
Mutex_m
|
|
192
|
+
]
|
|
193
|
+
stdlib_constants.any? { |c| name.start_with?(c) }
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Isort
|
|
4
|
+
# Validates Ruby syntax using the built-in parser
|
|
5
|
+
# Used by --atomic mode to ensure sorting doesn't introduce syntax errors
|
|
6
|
+
class SyntaxValidator
|
|
7
|
+
class << self
|
|
8
|
+
# Check if Ruby code has valid syntax
|
|
9
|
+
# Returns true if valid, false otherwise
|
|
10
|
+
def valid?(code)
|
|
11
|
+
check_syntax(code).nil?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Check syntax and return error message if invalid, nil if valid
|
|
15
|
+
def check_syntax(code)
|
|
16
|
+
# Use Ruby's built-in syntax check
|
|
17
|
+
catch(:valid) do
|
|
18
|
+
eval("BEGIN { throw :valid }; #{code}", nil, "(syntax_check)", 0)
|
|
19
|
+
end
|
|
20
|
+
nil
|
|
21
|
+
rescue SyntaxError => e
|
|
22
|
+
e.message
|
|
23
|
+
rescue StandardError
|
|
24
|
+
# Other errors during eval don't indicate syntax errors
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Check if a file has valid Ruby syntax
|
|
29
|
+
def valid_file?(file_path)
|
|
30
|
+
return false unless File.exist?(file_path)
|
|
31
|
+
|
|
32
|
+
content = File.read(file_path, encoding: "UTF-8")
|
|
33
|
+
valid?(content)
|
|
34
|
+
rescue Errno::ENOENT, Encoding::InvalidByteSequenceError
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Use Ruby's built-in -c flag for more accurate syntax checking
|
|
39
|
+
# This is safer than eval-based checking
|
|
40
|
+
def valid_with_ruby_c?(code)
|
|
41
|
+
require "open3"
|
|
42
|
+
require "tempfile"
|
|
43
|
+
|
|
44
|
+
Tempfile.create(["syntax_check", ".rb"]) do |f|
|
|
45
|
+
f.write(code)
|
|
46
|
+
f.flush
|
|
47
|
+
|
|
48
|
+
stdout, stderr, status = Open3.capture3("ruby", "-c", f.path)
|
|
49
|
+
status.success?
|
|
50
|
+
end
|
|
51
|
+
rescue StandardError
|
|
52
|
+
# If we can't run ruby -c, fall back to eval-based check
|
|
53
|
+
valid?(code)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Check syntax using ruby -c (more reliable but slower)
|
|
57
|
+
# Returns nil if valid, error message if invalid
|
|
58
|
+
def check_syntax_with_ruby_c(code)
|
|
59
|
+
require "open3"
|
|
60
|
+
require "tempfile"
|
|
61
|
+
|
|
62
|
+
Tempfile.create(["syntax_check", ".rb"]) do |f|
|
|
63
|
+
f.write(code)
|
|
64
|
+
f.flush
|
|
65
|
+
|
|
66
|
+
stdout, stderr, status = Open3.capture3("ruby", "-c", f.path)
|
|
67
|
+
status.success? ? nil : stderr.strip
|
|
68
|
+
end
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
# If we can't run ruby -c, fall back to eval-based check
|
|
71
|
+
check_syntax(code)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/isort/version.rb
CHANGED