馃殌 Grammar-Aware Code Formatter: Structure through separation (supports Go, JavaScript, TypeScript, JSX, and TSX)
go
formatter
code-formatter
javascript
typescript
jsx
tsx
1package main
2
3import (
4 "fmt"
5 "go/ast"
6 "go/token"
7)
8
9func isGeneralDeclarationScoped(generalDeclaration *ast.GenDecl) bool {
10 for _, specification := range generalDeclaration.Specs {
11 if typeSpecification, isTypeSpecification := specification.(*ast.TypeSpec); isTypeSpecification {
12 switch typeSpecification.Type.(type) {
13 case *ast.StructType, *ast.InterfaceType:
14 return true
15 }
16 }
17 }
18
19 return false
20}
21
22func (f *Formatter) buildLineInfo(tokenFileSet *token.FileSet, parsedFile *ast.File) map[int]*lineInformation {
23 lineInformationMap := make(map[int]*lineInformation, 2*len(parsedFile.Decls))
24 tokenFile := tokenFileSet.File(parsedFile.Pos())
25
26 if tokenFile == nil {
27 return lineInformationMap
28 }
29
30 for _, declaration := range parsedFile.Decls {
31 startLine := tokenFile.Line(declaration.Pos())
32 endLine := tokenFile.Line(declaration.End())
33 statementType := ""
34 isScoped := false
35
36 switch typedDeclaration := declaration.(type) {
37 case *ast.GenDecl:
38 statementType = typedDeclaration.Tok.String()
39 isScoped = isGeneralDeclarationScoped(typedDeclaration)
40 case *ast.FuncDecl:
41 statementType = "func"
42 isScoped = true
43 default:
44 statementType = fmt.Sprintf("%T", declaration)
45 }
46
47 lineInformationMap[startLine] = &lineInformation{statementType: statementType, isTopLevel: true, isScoped: isScoped, isStartLine: true}
48
49 if endLine != startLine {
50 lineInformationMap[endLine] = &lineInformation{statementType: statementType, isTopLevel: true, isScoped: isScoped, isStartLine: false}
51 }
52 }
53
54 ast.Inspect(parsedFile, func(astNode ast.Node) bool {
55 if astNode == nil {
56 return true
57 }
58
59 switch typedNode := astNode.(type) {
60 case *ast.BlockStmt:
61 f.processBlock(tokenFile, typedNode, lineInformationMap)
62 case *ast.CaseClause:
63 f.processStatementList(tokenFile, typedNode.Body, lineInformationMap)
64 case *ast.CommClause:
65 f.processStatementList(tokenFile, typedNode.Body, lineInformationMap)
66 }
67
68 return true
69 })
70
71 return lineInformationMap
72}
73
74func (f *Formatter) processBlock(tokenFile *token.File, blockStatement *ast.BlockStmt, lineInformationMap map[int]*lineInformation) {
75 if blockStatement == nil {
76 return
77 }
78
79 f.processStatementList(tokenFile, blockStatement.List, lineInformationMap)
80}
81
82func (f *Formatter) processStatementList(tokenFile *token.File, statements []ast.Stmt, lineInformationMap map[int]*lineInformation) {
83 for _, statement := range statements {
84 startLine := tokenFile.Line(statement.Pos())
85 endLine := tokenFile.Line(statement.End())
86 statementType := ""
87 isScoped := false
88
89 switch typedStatement := statement.(type) {
90 case *ast.DeclStmt:
91 if generalDeclaration, isGeneralDeclaration := typedStatement.Decl.(*ast.GenDecl); isGeneralDeclaration {
92 statementType = generalDeclaration.Tok.String()
93 } else {
94 statementType = fmt.Sprintf("%T", statement)
95 }
96 case *ast.IfStmt, *ast.ForStmt, *ast.RangeStmt, *ast.SwitchStmt,
97 *ast.TypeSwitchStmt, *ast.SelectStmt, *ast.BlockStmt:
98 statementType = fmt.Sprintf("%T", statement)
99 isScoped = true
100 default:
101 statementType = fmt.Sprintf("%T", statement)
102 }
103
104 existingStart := lineInformationMap[startLine]
105
106 if existingStart == nil || !existingStart.isStartLine {
107 lineInformationMap[startLine] = &lineInformation{statementType: statementType, isTopLevel: false, isScoped: isScoped, isStartLine: true}
108 }
109
110 existingEnd := lineInformationMap[endLine]
111
112 if existingEnd == nil || !existingEnd.isStartLine {
113 lineInformationMap[endLine] = &lineInformation{statementType: statementType, isTopLevel: false, isScoped: isScoped, isStartLine: false}
114 }
115
116 switch typedStatement := statement.(type) {
117 case *ast.IfStmt:
118 f.processBlock(tokenFile, typedStatement.Body, lineInformationMap)
119
120 if typedStatement.Else != nil {
121 if elseBlock, isBlockStatement := typedStatement.Else.(*ast.BlockStmt); isBlockStatement {
122 f.processBlock(tokenFile, elseBlock, lineInformationMap)
123 } else if elseIfStatement, isIfStatement := typedStatement.Else.(*ast.IfStmt); isIfStatement {
124 f.processIfStatement(tokenFile, elseIfStatement, lineInformationMap)
125 }
126 }
127 case *ast.ForStmt:
128 f.processBlock(tokenFile, typedStatement.Body, lineInformationMap)
129 case *ast.RangeStmt:
130 f.processBlock(tokenFile, typedStatement.Body, lineInformationMap)
131 case *ast.SwitchStmt:
132 f.processBlock(tokenFile, typedStatement.Body, lineInformationMap)
133 case *ast.TypeSwitchStmt:
134 f.processBlock(tokenFile, typedStatement.Body, lineInformationMap)
135 case *ast.SelectStmt:
136 f.processBlock(tokenFile, typedStatement.Body, lineInformationMap)
137 case *ast.BlockStmt:
138 f.processBlock(tokenFile, typedStatement, lineInformationMap)
139 }
140 }
141}
142
143func (f *Formatter) processIfStatement(tokenFile *token.File, ifStatement *ast.IfStmt, lineInformationMap map[int]*lineInformation) {
144 startLine := tokenFile.Line(ifStatement.Pos())
145 endLine := tokenFile.Line(ifStatement.End())
146 existingStart := lineInformationMap[startLine]
147
148 if existingStart == nil || !existingStart.isStartLine {
149 lineInformationMap[startLine] = &lineInformation{statementType: "*ast.IfStmt", isTopLevel: false, isScoped: true, isStartLine: true}
150 }
151
152 existingEnd := lineInformationMap[endLine]
153
154 if existingEnd == nil || !existingEnd.isStartLine {
155 lineInformationMap[endLine] = &lineInformation{statementType: "*ast.IfStmt", isTopLevel: false, isScoped: true, isStartLine: false}
156 }
157
158 f.processBlock(tokenFile, ifStatement.Body, lineInformationMap)
159
160 if ifStatement.Else != nil {
161 if elseBlock, isBlockStatement := ifStatement.Else.(*ast.BlockStmt); isBlockStatement {
162 f.processBlock(tokenFile, elseBlock, lineInformationMap)
163 } else if elseIfStatement, isIfStatement := ifStatement.Else.(*ast.IfStmt); isIfStatement {
164 f.processIfStatement(tokenFile, elseIfStatement, lineInformationMap)
165 }
166 }
167}