Skip to content

Commit f004aa5

Browse files
authored
Fix sorting self import alias and importing unicode (#17)
1 parent 457ed1e commit f004aa5

3 files changed

Lines changed: 110 additions & 38 deletions

File tree

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "ITensorFormatter"
22
uuid = "b6bf39f1-c9d3-4bad-aad8-593d802f65fd"
3-
version = "0.2.10"
3+
version = "0.2.11"
44
authors = ["ITensor developers <support@itensor.org> and contributors"]
55

66
[workspace]

src/ITensorFormatter.jl

Lines changed: 65 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ const JULIAFORMATTER_OPTIONS = (
4747
trailing_zero = true,
4848
)
4949

50-
is_using_or_import(x) = kind(x) === K"using" || kind(x) === K"import"
50+
is_using_or_import(x::SyntaxNode) = kind(x) === K"using" || kind(x) === K"import"
5151

52-
function find_using_or_import(x)
52+
function find_using_or_import(x::SyntaxNode)
5353
if is_using_or_import(x)
5454
return x.parent
5555
elseif iszero(length(children(x)))
@@ -63,46 +63,62 @@ function find_using_or_import(x)
6363
end
6464
end
6565

66-
char_range(x) = x.position:(x.position + span(x) - 1)
66+
# JuliaSyntax nodes report (position, span) in *bytes*.
67+
# Julia strings must be sliced using *valid string indices* (start bytes of UTF-8 chars).
68+
#
69+
# Convert a node's byte range into a safe `UnitRange` of valid string indices.
70+
# (O(1): fix endpoints with `thisind`.)
71+
function node_char_range(n::SyntaxNode, src::AbstractString)
72+
startb = n.position
73+
nspan = span(n)
74+
nspan <= 0 && return startb:(startb - 1) # empty range
75+
stopb = min(startb + nspan - 1, ncodeunits(src))
76+
return thisind(src, startb):thisind(src, stopb)
77+
end
6778

68-
function organize_import_blocks_string(s)
69-
jst = parseall(SyntaxNode, s)
79+
function organize_import_blocks_string(s::AbstractString)
80+
jst = parseall(SyntaxNode, String(s))
7081
return organize_import_blocks(jst)
7182
end
72-
organize_import_blocks_file(f) = organize_import_blocks_string(read(f, String))
7383

74-
# Sort symbols, but keep the module self-reference first if present
75-
function sort_with_self_first(syms, self)
76-
self′ = pop!(syms, self, nothing)
77-
sorted = sort!(collect(syms))
78-
if self′ !== nothing
79-
pushfirst!(sorted, self)
80-
end
81-
return sorted
84+
function organize_import_blocks_file(f::AbstractString)
85+
return organize_import_blocks_string(read(f, String))
86+
end
87+
88+
# Sort symbols, but keep the module self-reference (bare and all aliases) first if present
89+
function sort_with_self_first(syms::Vector{String}, self::String)
90+
syms = unique(syms)
91+
selfs = self in syms ? [self] : String[]
92+
self_aliases = sort!(filter(s -> startswith(s, self * " as "), syms))
93+
rest = sort!(setdiff(syms, [selfs; self_aliases]))
94+
return [selfs; self_aliases; rest]
8295
end
8396

8497
# Organize a single block of adjacent import/using statements
85-
function organize_import_block(siblings, node_text)
98+
function organize_import_block(siblings::AbstractVector{<:SyntaxNode}, node_text)
8699
using_mods = Set{String}()
87-
using_syms = Dict{String, Set{String}}()
100+
using_syms = Dict{String, Vector{String}}()
88101
import_mods = Set{String}()
89-
import_syms = Dict{String, Set{String}}()
102+
import_syms = Dict{String, Vector{String}}()
90103

91104
for s in siblings
92105
isusing = kind(s) === K"using"
93106
for a in children(s)
94107
if kind(a) === K":"
95108
a_args = children(a)
96-
mod = node_text(a_args[1])
97-
set = get!(Set, isusing ? using_syms : import_syms, mod)
109+
mod = String(node_text(a_args[1]))
110+
set = get!(() -> String[], isusing ? using_syms : import_syms, mod)
98111
for i in 2:length(a_args)
99112
push!(set, String(node_text(a_args[i])))
100113
end
101114
elseif kind(a) === K"." || kind(a) === K"importpath"
102115
push!(isusing ? using_mods : import_mods, String(node_text(a)))
103116
elseif !isusing && kind(a) === K"as"
104117
a_args = children(a)
105-
push!(import_mods, node_text(a_args[1]) * " as " * node_text(a_args[end]))
118+
push!(
119+
import_mods,
120+
String(node_text(a_args[1])) * " as " * String(node_text(a_args[end]))
121+
)
106122
else
107123
error("Unexpected syntax in using/import statement.")
108124
end
@@ -124,21 +140,26 @@ function organize_import_block(siblings, node_text)
124140
push!(using_lines, "using " * m * ": " * join(sort_with_self_first(s, m), ", "))
125141
end
126142
io = IOBuffer()
127-
join(io, sort!(import_lines), "\n")
128-
length(import_lines) > 0 && length(using_lines) > 0 && print(io, "\n")
129-
join(io, sort!(using_lines), "\n")
143+
if !isempty(import_lines)
144+
print(io, join(sort!(import_lines), "\n"))
145+
end
146+
if !isempty(import_lines) && !isempty(using_lines)
147+
print(io, "\n")
148+
end
149+
if !isempty(using_lines)
150+
print(io, join(sort!(using_lines), "\n"))
151+
end
130152
return String(take!(io))
131153
end
132154

133-
function organize_import_blocks(input)
134-
src = JuliaSyntax.sourcetext(input)
155+
function organize_import_blocks(input::SyntaxNode)
156+
# Keep a stable copy for slicing: node positions/spans refer to this text.
157+
src0 = JuliaSyntax.sourcetext(input)
135158
x = find_using_or_import(input)
136-
isnothing(x) && return src
137-
159+
isnothing(x) && return src0
138160
child_nodes = children(x)
139-
140161
# Find all groups of adjacent import/using statements
141-
groups = Vector{Any}[]
162+
groups = Vector{SyntaxNode}[]
142163
i = 1
143164
while i <= length(child_nodes)
144165
if is_using_or_import(child_nodes[i])
@@ -151,18 +172,25 @@ function organize_import_blocks(input)
151172
i += 1
152173
end
153174
end
154-
155-
# Extract the source text of a node, trimming whitespace
156-
node_text(n) = strip(src[char_range(n)])
157-
175+
# Extract the source text of a node, trimming whitespace (Unicode-safe).
176+
# Always slice from src0 (stable offsets), not the rewritten `src`.
177+
node_text(n::SyntaxNode) = strip(src0[node_char_range(n, src0)])
178+
# Rewritten output source.
179+
src = src0
158180
# Process each group from right to left to preserve positions
159181
for siblings in reverse(groups)
160182
formatted = organize_import_block(siblings, node_text)
161-
first_pos = first(char_range(siblings[1]))
162-
last_pos = last(char_range(siblings[end]))
163-
src = src[1:(first_pos - 1)] * chomp(formatted) * src[(last_pos + 1):end]
183+
# Compute splice bounds using src0 (node offsets), then splice into src.
184+
first_pos = first(node_char_range(first(siblings), src0))
185+
last_pos = last(node_char_range(last(siblings), src0))
186+
# Unicode-safe splice boundaries (never do ±1 on raw integer indices)
187+
before =
188+
first_pos == firstindex(src) ? "" :
189+
src[firstindex(src):prevind(src, first_pos)]
190+
after_start = nextind(src, last_pos)
191+
after = after_start > lastindex(src) ? "" : src[after_start:end]
192+
src = before * chomp(formatted) * after
164193
end
165-
166194
return src
167195
end
168196

test/test_basics.jl

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,50 @@ organize(s) = ITensorFormatter.organize_import_blocks_string(s)
128128
@test result ==
129129
"using Baz: baz\nusing Foo: foo\nx = 1\nusing Alpha: a\nusing Zebra: a, z\n"
130130
end
131+
132+
@testset "multiple self-imports and aliases" begin
133+
# Bare self and aliases, plus others
134+
result = organize("using Foo: bar, Foo as Q, Foo, Foo as P, baz, A")
135+
@test result == "using Foo: Foo, Foo as P, Foo as Q, A, bar, baz"
136+
137+
# Only aliases, no bare self
138+
result = organize("using Foo: Foo as Q, Foo as P, baz, A")
139+
@test result == "using Foo: Foo as P, Foo as Q, A, baz"
140+
141+
# Only bare self
142+
result = organize("using Foo: Foo, baz, A")
143+
@test result == "using Foo: Foo, A, baz"
144+
145+
# Duplicates should be removed
146+
result = organize("using Foo: Foo, Foo as P, Foo as P, Foo, A, A")
147+
@test result == "using Foo: Foo, Foo as P, A"
148+
end
149+
150+
@testset "unicode and non-ASCII symbols" begin
151+
# Single unicode symbol
152+
result = organize("using Foo: ⊗")
153+
@test result == "using Foo: ⊗"
154+
155+
# Multiple unicode and ASCII symbols
156+
result = organize("using Foo: ⊗, x, y, X")
157+
@test result == "using Foo: X, x, y, ⊗"
158+
159+
# Multiple unicode symbols, ASCII symbols, and self imports
160+
result = organize("using Foo: ⊗, Foo as B, x, ×, Foo, y, Foo as A, X")
161+
@test result == "using Foo: Foo, Foo as A, Foo as B, X, x, y, ×, ⊗"
162+
163+
# Unicode self-reference
164+
result = organize("using ⊗: ⊗, x, y")
165+
@test result == "using ⊗: ⊗, x, y"
166+
167+
# Unicode self-reference with alias
168+
result = organize("using ⊗: ⊗ as T, ⊗, x, y")
169+
@test result == "using ⊗: ⊗, ⊗ as T, x, y"
170+
171+
# Unicode and ASCII, with alias
172+
result = organize("using Foo: ⊗ as T, x, ⊗, y")
173+
@test result == "using Foo: x, y, ⊗, ⊗ as T"
174+
end
131175
end
132176

133177
@testset "main" begin

0 commit comments

Comments
 (0)