Skip to content

Commit 7841400

Browse files
authored
Small fixes and tweaks (#1)
1 parent 2c983f7 commit 7841400

7 files changed

Lines changed: 203 additions & 64 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.1.0"
3+
version = "0.2.0"
44
authors = ["ITensor developers <support@itensor.org> and contributors"]
55

66
[workspace]

docs/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ ITensorFormatter = {path = ".."}
99
[compat]
1010
Documenter = "1"
1111
Literate = "2"
12-
ITensorFormatter = "0.1"
12+
ITensorFormatter = "0.2"

examples/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ ITensorFormatter = "b6bf39f1-c9d3-4bad-aad8-593d802f65fd"
55
ITensorFormatter = {path = ".."}
66

77
[compat]
8-
ITensorFormatter = "0.1"
8+
ITensorFormatter = "0.2"

examples/README.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# # ITensorFormatter.jl
2-
#
2+
#
33
# [![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://itensor.github.io/ITensorFormatter.jl/stable/)
44
# [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://itensor.github.io/ITensorFormatter.jl/dev/)
55
# [![Build Status](https://github.com/ITensor/ITensorFormatter.jl/actions/workflows/Tests.yml/badge.svg?branch=main)](https://github.com/ITensor/ITensorFormatter.jl/actions/workflows/Tests.yml?query=branch%3Amain)

src/ITensorFormatter.jl

Lines changed: 75 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
module ITensorFormatter
22

3-
using JuliaFormatter
4-
using JuliaSyntax: @K_str, JuliaSyntax, SyntaxNode, children, kind, parseall, span
5-
using Runic
3+
if VERSION >= v"1.11.0-DEV.469"
4+
let str = "public main"
5+
eval(Meta.parse(str))
6+
end
7+
end
8+
9+
using JuliaFormatter: JuliaFormatter
10+
using JuliaSyntax: JuliaSyntax, @K_str, SyntaxNode, children, kind, parseall, span
11+
using Runic: Runic
612

713
function is_using_or_import(x)
814
return kind(x) === K"using" || kind(x) === K"import"
@@ -22,7 +28,7 @@ function find_using_or_import(x)
2228
end
2329
end
2430

25-
char_range(x) = x.position:(x.position + span(x))
31+
char_range(x) = x.position:(x.position + span(x) - 1)
2632

2733
function organize_import_file(f)
2834
jst = parseall(SyntaxNode, read(f, String))
@@ -33,50 +39,49 @@ function organize_import_block(input)
3339
# Collect all sibling blocks that are also using/import expressions
3440

3541
x = find_using_or_import(input)
36-
isnothing(x) && return
42+
isnothing(x) && return JuliaSyntax.sourcetext(input)
3743

3844
siblings = []
3945

4046
child_nodes = children(x)
41-
ind = findfirst(is_using_or_import, child_nodes)
47+
first_ind = findfirst(is_using_or_import, child_nodes)
4248

43-
while !isnothing(ind)
44-
push!(siblings, popat!(child_nodes, ind))
45-
ind = findfirst(is_using_or_import, child_nodes)
49+
for ind in first_ind:length(child_nodes)
50+
if is_using_or_import(child_nodes[ind])
51+
push!(siblings, child_nodes[ind])
52+
else
53+
break
54+
end
4655
end
4756

57+
src = JuliaSyntax.sourcetext(input)
58+
4859
# Collect all modules and symbols
4960
using_mods = Set{String}()
5061
using_syms = Dict{String, Set{String}}()
5162
import_mods = Set{String}()
5263
import_syms = Dict{String, Set{String}}()
5364

54-
# Joins e.g. [".", ".", "Foo", "Bar"] (from "using ..Foo.Bar") to "..Foo.Bar"
55-
function module_join(x)
56-
io = IOBuffer()
57-
for y in children(x)[1:(end - 1)]
58-
print(io, y.val)
59-
y.val == "." && continue
60-
print(io, ".")
61-
end
62-
print(io, children(x)[end].val)
63-
return String(take!(io))
64-
end
65+
# Extract the source text of a node, trimming whitespace
66+
node_text(x) = strip(src[char_range(x)])
6567

6668
for s in siblings
6769
isusing = kind(s) === K"using"
6870
for a in children(s)
6971
if kind(a) === K":"
7072
a_args = children(a)
71-
mod = module_join(a_args[1])
73+
mod = node_text(a_args[1])
7274
set = get!(Set, isusing ? using_syms : import_syms, mod)
7375
for i in 2:length(a_args)
74-
push!(set, join(y.val for y in children(a_args[i])))
76+
push!(set, String(node_text(a_args[i])))
7577
end
76-
elseif kind(a) === K"."
77-
push!(isusing ? using_mods : import_mods, module_join(a))
78+
elseif kind(a) === K"." || kind(a) === K"importpath"
79+
push!(isusing ? using_mods : import_mods, String(node_text(a)))
80+
elseif !isusing && kind(a) === K"as"
81+
a_args = children(a)
82+
push!(import_mods, node_text(a_args[1]) * " as " * node_text(a_args[end]))
7883
else
79-
error("unreachable?")
84+
error("Unexpected syntax in using/import statement.")
8085
end
8186
end
8287
end
@@ -86,66 +91,78 @@ function organize_import_block(input)
8691
# BlueStyle (modules, types, ..., functions) since usually CamelCase is used for
8792
# modules, types, etc, but possibly this can be improved by using information
8893
# available from SymbolServer
94+
# Sort symbols, but keep the module self-reference first if present
95+
function sort_with_self_first(syms, self)
96+
self′ = pop!(syms, self, nothing)
97+
sorted = sort!(collect(syms))
98+
if self′ !== nothing
99+
pushfirst!(sorted, self)
100+
end
101+
return sorted
102+
end
103+
89104
import_lines = String[]
90105
for m in import_mods
91106
push!(import_lines, "import " * m)
92107
end
93108
for (m, s) in import_syms
94-
push!(import_lines, "import " * m * ": " * join(sort!(collect(s)), ", "))
109+
push!(import_lines, "import " * m * ": " * join(sort_with_self_first(s, m), ", "))
95110
end
96111
using_lines = String[]
97112
for m in using_mods
98113
push!(using_lines, "using " * m)
99114
end
100115
for (m, s) in using_syms
101-
push!(using_lines, "using " * m * ": " * join(sort!(collect(s)), ", "))
116+
push!(using_lines, "using " * m * ": " * join(sort_with_self_first(s, m), ", "))
102117
end
103118
io = IOBuffer()
104119
join(io, sort!(import_lines), "\n")
105-
length(import_lines) > 0 && print(io, "\n\n")
120+
length(import_lines) > 0 && length(using_lines) > 0 && print(io, "\n")
106121
join(io, sort!(using_lines), "\n")
107122
str_to_fmt = String(take!(io))
108123

109124
# Line wrap the using/import statements only
110125
formatted = JuliaFormatter.format_text(str_to_fmt; join_lines_based_on_source = true)
111126

112-
src = JuliaSyntax.sourcetext(input)
113-
114-
statement_range = char_range(siblings[1])
115127
first_pos = first(char_range(siblings[1]))
128+
last_pos = last(char_range(siblings[end]))
116129

117-
content = src[1:first_pos-1] * formatted
118-
119-
content_to_append = ""
120-
121-
last_pos = last(char_range(siblings[1]))
122-
123-
# Get the content between the using/import statements
124-
for s in siblings[2:end]
125-
statement_range = char_range(s)
126-
content_to_append *= src[last_pos + 1:(first(statement_range) - 1)]
127-
last_pos = last(statement_range)
128-
end
129-
130-
if length(content_to_append) > 1
131-
content_to_append = "\n" * content_to_append
132-
end
133-
134-
# Tack this onto the end
135-
content *= content_to_append * src[(last_pos + 1):end]
130+
content = src[1:(first_pos - 1)] * chomp(formatted) * src[(last_pos + 1):end]
136131

137132
return content
138133
end
139134

140-
function (@main)(ARGS)
141-
for file in ARGS
142-
_, ext = splitext(file)
143-
ext == ".jl" || error("Only .jl files are supported")
144-
content = organize_import_file(file)
145-
write(file, content)
135+
"""
136+
ITensorFormatter.main(argv)
137+
138+
Format Julia source files. Primarily formats using Runic formatting, but additionally
139+
organizes using/import statements by merging adjacent blocks, sorting modules and symbols,
140+
and line-wrapping. Accepts file paths and directories as arguments. Options starting with
141+
`--` are forwarded to Runic, see the
142+
[Runic documentation](https://github.com/fredrikekre/Runic.jl) for more details.
143+
"""
144+
function main(argv)
145+
inputfiles = String[]
146+
x = filter(!startswith("--"), argv)
147+
for x in argv
148+
if startswith(x, "--")
149+
# Ignore options for now, they are assumed to be for Runic.
150+
elseif isdir(x)
151+
Runic.scandir!(inputfiles, x)
152+
else # isfile(x)
153+
push!(inputfiles, x) # Assume it is a file for now
154+
end
155+
end
156+
for inputfile in inputfiles
157+
content = organize_import_file(inputfile)
158+
write(inputfile, content)
146159
end
147-
Runic.main(ARGS)
160+
Runic.main(argv)
148161
return 0
149-
end
162+
end
163+
164+
@static if isdefined(Base, Symbol("@main"))
165+
@main
166+
end
150167

151168
end

test/Project.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[deps]
22
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
33
ITensorFormatter = "b6bf39f1-c9d3-4bad-aad8-593d802f65fd"
4+
JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4"
45
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
56
Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb"
67
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
@@ -10,7 +11,8 @@ ITensorFormatter = {path = ".."}
1011

1112
[compat]
1213
Aqua = "0.8"
13-
ITensorFormatter = "0.1"
14+
ITensorFormatter = "0.2"
15+
JuliaSyntax = "0.4.10"
1416
SafeTestsets = "0.1"
1517
Suppressor = "0.2"
1618
Test = "1.10"

test/test_basics.jl

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,126 @@
11
using ITensorFormatter: ITensorFormatter
2+
using JuliaSyntax: SyntaxNode, parseall
23
using Test: @test, @testset
34

5+
function organize(s)
6+
jst = parseall(SyntaxNode, s)
7+
return ITensorFormatter.organize_import_block(jst)
8+
end
9+
410
@testset "ITensorFormatter" begin
5-
# Tests go here.
11+
@testset "no imports" begin
12+
@test organize("x = 1\n") == "x = 1\n"
13+
end
14+
15+
@testset "single using with colon" begin
16+
@test organize("using Foo: bar\n") == "using Foo: bar\n"
17+
end
18+
19+
@testset "single import with colon" begin
20+
@test organize("import Foo: bar\n") == "import Foo: bar\n"
21+
end
22+
23+
@testset "bare using Package" begin
24+
@test organize("using Foo\n") == "using Foo\n"
25+
end
26+
27+
@testset "bare import Package" begin
28+
@test organize("import Foo\n") == "import Foo\n"
29+
end
30+
31+
@testset "sort symbols alphabetically" begin
32+
result = organize("using Foo: baz, bar\n")
33+
@test result == "using Foo: bar, baz\n"
34+
end
35+
36+
@testset "self-reference stays first" begin
37+
result = organize("using Foo: bar, Foo\n")
38+
@test result == "using Foo: Foo, bar\n"
39+
end
40+
41+
@testset "sort mixed symbols with self-reference" begin
42+
result = organize("using Foo: baz, Foo, Bar, bar\n")
43+
@test result == "using Foo: Foo, Bar, bar, baz\n"
44+
end
45+
46+
@testset "merge duplicate using statements" begin
47+
result = organize("using Foo: bar\nusing Foo: baz\n")
48+
@test result == "using Foo: bar, baz\n"
49+
end
50+
51+
@testset "merge duplicate import statements" begin
52+
result = organize("import Foo: bar\nimport Foo: baz\n")
53+
@test result == "import Foo: bar, baz\n"
54+
end
55+
56+
@testset "import before using" begin
57+
result = organize("using Foo: foo\nimport Bar: bar\n")
58+
@test result == "import Bar: bar\nusing Foo: foo\n"
59+
end
60+
61+
@testset "preserve code after imports" begin
62+
result = organize("using Foo: bar\nx = 1\n")
63+
@test result == "using Foo: bar\nx = 1\n"
64+
end
65+
66+
@testset "preserve code before imports" begin
67+
result = organize("x = 1\nusing Foo: bar\n")
68+
@test result == "x = 1\nusing Foo: bar\n"
69+
end
70+
71+
@testset "relative import using .Package" begin
72+
result = organize("using .Foo: bar\n")
73+
@test result == "using .Foo: bar\n"
74+
end
75+
76+
@testset "relative import using ..Package" begin
77+
result = organize("using ..Foo: bar\n")
78+
@test result == "using ..Foo: bar\n"
79+
end
80+
81+
@testset "bare relative using" begin
82+
result = organize("using .Foo\n")
83+
@test result == "using .Foo\n"
84+
end
85+
86+
@testset "dotted module path" begin
87+
result = organize("using Foo.Bar: baz\n")
88+
@test result == "using Foo.Bar: baz\n"
89+
end
90+
91+
@testset "import as" begin
92+
result = organize("import Foo as Bar\n")
93+
@test result == "import Foo as Bar\n"
94+
end
95+
96+
@testset "import relative as" begin
97+
result = organize("import .Foo as Bar\n")
98+
@test result == "import .Foo as Bar\n"
99+
end
100+
101+
@testset "bare using and using with colon" begin
102+
result = organize("using Foo\nusing Bar: baz\n")
103+
@test result == "using Bar: baz\nusing Foo\n"
104+
end
105+
106+
@testset "only adjacent imports are collected" begin
107+
input = "using Foo: foo\nx = 1\nusing Bar: bar\n"
108+
result = organize(input)
109+
@test result == "using Foo: foo\nx = 1\nusing Bar: bar\n"
110+
end
111+
112+
@testset "sort multiple using lines" begin
113+
result = organize("using Zebra: z\nusing Alpha: a\n")
114+
@test result == "using Alpha: a\nusing Zebra: z\n"
115+
end
116+
117+
@testset "sort multiple import lines" begin
118+
result = organize("import Zebra: z\nimport Alpha: a\n")
119+
@test result == "import Alpha: a\nimport Zebra: z\n"
120+
end
121+
122+
@testset "mixed bare and colon imports" begin
123+
result = organize("import Foo\nimport Bar: baz\n")
124+
@test result == "import Bar: baz\nimport Foo\n"
125+
end
6126
end

0 commit comments

Comments
 (0)